let appConfig = {
    host: 'http://sz.emtailor.com:9068/',
    // host: 'http://localhost:9068/',
    // host: 'http://e.7mo.org:9068/',
    /*
    客户端类型: client,middle,file
    client: 1.客户端模式,不进行心跳;
    middle: 1.中间件模式,进行心跳;
    file:   1.文件端模式,不进行心跳;
    one:    4.一体端模式,不进行心跳;
     */
    serviceType: 'one',
    /*
    是否debug模式: true,false
    影响一些日志的输出
     */
    isDebug: false,
    /*
    是否全局结束所有任务,默认false
     */
    allStop: false,
    /*
    axios全局超时时间ms
     */
    axiosTimeOut: 60 * 1000,
    /*
    心跳惩罚时间秒
     */
    heartBeatPunish: 10,
    heartBeatFixedDelay: 10,
    /*
    用户解封时间ms
     */
    userUnBanIntervalMs: 172800000,
    /*
    默认任务间隔ms
     */
    defaultSleepTaskMs: 86400000,
    defaultUserCheckUrl: 'https://www.instagram.com/graphql/query/?query_id=17888483320059182&id=4143607182&first=40',
    /*
    忽略图片下载失败
     */
    ignoreGetImage: true,
    /*
    是否健康检查(自动启动任务)
     */
    isHealthCheck: true,
    /*
    健康检查周期(阀值,超过此时间将尝试启动任务)
     */
    healthCheckIntervalMs: 10 * 60 * 1000,
    /*
    邮件告警间隔(触发第多少次发送邮件,才发送邮件;不要让邮件发送太频繁,添加容错范围)
     */
    emailCountIgnore: 6,
};String.prototype.reverse = function () {
    return this.split('').reverse().join('')
};

String.prototype.toSize = function () {
    let size = this;
    if (!!size && size > 0) {
        const KB = 1024;
        const MB = 1024 * KB;
        const GB = 1024 * MB;
        const TB = 1024 * GB;
        let sizeStr = size > TB ? size / TB + "TB" : size > GB ? size / GB + "GB" : size > MB ? size / MB + "MB" : size > KB ? size / KB + "KB" : size;
        let unit = sizeStr.substr(-2);
        let number = sizeStr.replace(unit, '');
        number = Math.floor(number * 10) / 10;
        sizeStr = number + unit;
        return sizeStr;
    } else {
        return '0KB';
    }
};

String.prototype.between = function (start = '', end = '', isInner = true) {
    let str = this;
    if (!!str && str.length > 0) {
        let startIndex = 0;
        if (start) {
            let io = str.indexOf(start);
            if (io >= 0) {
                if (isInner) {
                    io = io + start.length;
                }
                startIndex = io;
            } else {
                return '';
            }
        }
        let endIndex = str.length;
        if (end) {
            let io = str.indexOf(end, startIndex);
            if (io >= 0) {
                if (!isInner) {
                    io = io + end.length;
                }
                endIndex = io;
            } else {
                return '';
            }
        }
        return str.substring(startIndex, endIndex);
    } else {
        return '';
    }
};

let UUID = (len) => {
    let max = 520 << 520;
    len = len === null || isNaN(len) || len > max ? 6 : len;
    let uuid = '';
    let rand = () => ('' + Math.random()).substring(2);
    do {
        uuid += rand();
    } while (uuid.length < len) ;
    return uuid.substring(0, len);
};

let trimQuote = (str) => {
    let trimStr = String(str).trim();
    if (!!trimStr) {
        let quote = '"';
        if (trimStr.startsWith(quote)) {
            trimStr = trimStr.substr(1);
        }
        if (trimStr.endsWith(quote)) {
            trimStr = trimStr.substring(0, trimStr.lastIndexOf(quote));
        }
    }
    return trimStr;
};

/**
 * 获取字符串的 哈希值
 * @param str 字符串
 * @param caseSensitive 区分大小写
 * @returns {number}
 */
let getHashCode = (str, caseSensitive = true) => {
    str = '' + str;
    if (!caseSensitive) {
        str = str.toLowerCase();
    }
    let hash = 13709061665, i, ch;
    for (i = str.length - 1; i >= 0; i--) {
        ch = str.charCodeAt(i);
        hash ^= ((hash << 5) + ch + (hash >> 2));
    }
    return (hash & 2147483647);
};

/**
 * fake flat for webStorm,idea
 * @type {string}
 */
let fakeU = 'fake webStorm,idea';
if (!fakeU) {
    Array.prototype.flat = () => Array.prototype.flat();
    /**
     *
     * @param url{string}
     * @param param{object}
     * @returns {object}
     */
    let axiosParam = (url, param = {}) => Object.assign(url, param);
    /**
     *
     * @type {{request: (function(string, Object): string & Object), post: (function(string, Object): string & Object), get: (function(string, Object): string & Object), delete: (function(string, Object): string & Object), put: (function(string, Object): string & Object)}}
     */
    axios = {
        get: axiosParam,
        put: axiosParam,
        post: axiosParam,
        delete: axiosParam,
        request: axiosParam,
    };
    Vue = {
        getCurrentInstance: () => 0,
        // @see <a href="https://blog.csdn.net/weixin_34370347/article/details/89617464">Vue获取组件name属性</a>
        $options: {},
        globalProperties: {},
        $emit: () => 0,
        mount: () => 0,
        unmount: () => 0,
        createApp: () => 0,
        reactive: () => 0,
        $el: {},
    };
    /**
     * js-sha1
     * @see https://github.com/emn178/js-sha1
     * @param str
     * @returns {string}
     */
    sha1 = (str) => '';
    VueRouter = {
        createRouter: () => 0,
        createWebHashHistory: () => 0,
    };
    /**
     * js 代码格式化工具
     * @link https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.11.0/beautify.min.js
     * @param jsCode js脚本文本
     * @returns {string}
     */
    js_beautify = (jsCode) => String(jsCode);
}

/**
 * 验证脚本合法性
 * @returns {boolean}
 */
String.prototype.validScript = function () {
    let scriptStrObj = this;
    try {
        // 这里要转换为字符串才能正确验证, 否则传入的是String()对象, 无法验证
        eval(String(scriptStrObj));
        return true;
    } catch (e) {
        console.error('无效的脚本: %o, error: {}', scriptStrObj, e);
    }
    return false;
};

/**
 *  禁止用F5键
 * @returns {boolean}
 */
document.onkeydown = () => {
    console.log('event.keyCode', event.keyCode);
    if (event.keyCode === 116) {
        //event.keyCode = 0;
        //event.cancelBubble = true;
        //// window.location.href = '/';
        // return false;
    }
};

/**
 * 从数组中的对象中查找key键名对应的值包含被查找的值数组
 * @param sourceArr 源对象数组
 * @param key 数组中对象的key键名
 * @param valuesArr key的值数组
 * @returns {*}
 */
let findKey = (sourceArr, key, valuesArr) => sourceArr.filter(e => valuesArr.filter(f => f === e[key]).length === 1);

let randNumber = (min, max) => parseInt(Math.random() * (max - min + 1) + min, 10);

let deepClone = (obj) => JSON.parse(JSON.stringify(obj));

/**
 * 日期转指定格式字符串
 * @param fmt 日期格式化函数
 * @returns {string} 格式化后的日期字符串
 * @modified zhongbo
 * @date 2020/5/22
 */
Date.prototype.fmt = function dateFormat(fmt = 'MM月dd日HH:mm:ss,SSS') {
    //eg: yyyy-MM-dd HH:mm:ss
    let date = this;
    let ret;
    let opt = {
        "y+": date.getFullYear().toString(),        // 年
        "Y+": date.getFullYear().toString(),        // 年
        "M+": (date.getMonth() + 1).toString(),     // 月
        "d+": date.getDate().toString(),            // 日
        "D+": date.getDate().toString(),            // 日
        "H+": date.getHours().toString(),           // 时
        "m+": date.getMinutes().toString(),         // 分
        "s+": date.getSeconds().toString(),          // 秒
        "S+": date.getMilliseconds().toString()          // 秒
        // 有其他格式化字符需求可以继续添加，必须转化成字符串
    };
    for (let k in opt) {
        ret = new RegExp("(" + k + ")").exec(fmt);
        if (ret) {
            fmt = fmt.replace(ret[1], (ret[1].length === 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0")))
        }
    }
    return fmt;
};

/**
 * 秒转时间字符串
 * @param second
 * @returns {string}
 */
let timeUnit = (second) => {
    if (!!Math.floor(second)) {
        const MINUTE = 60;
        const HOUR = 60 * MINUTE;
        const DAY = 24 * HOUR;
        let timeStr = second > DAY ? second / DAY + '天' : second > HOUR ? second / HOUR + '时' : second > MINUTE ? second / MINUTE + '分' : second + '秒';
        let unit = timeStr.substr(-1);
        let number = timeStr.replace(unit, '');
        number = Math.floor(number * 10) / 10;
        timeStr = number + unit;
        return timeStr;
    } else {
        return '0秒'
    }
};

/**
 * 按键事件检测
 *
 * 特殊按键说明
 * metaKey : 即Win键
 * ctrlKey : 即Ctrl键
 * altKey : 即Alt键
 * shiftKey : 即Shift键
 * 'Tab' : 即Tab键
 * 'Escape' : 即Esc键
 * @param {KeyboardEvent} event
 * @param {array} param
 * @return {boolean}
 */
let isEventKey = (event, param = []) => {
    let matchAllKey = true;
    // 判断 event 按键事件需要使用 event.nativeEvent 来判断事件类型
    if (!!event && !!param && event.nativeEvent instanceof KeyboardEvent && param instanceof Array) {
        param.forEach(key => {
            if (matchAllKey) {
                if ('ctrl' === key) {
                    matchAllKey = event.ctrlKey;
                } else if ('alt' === key) {
                    matchAllKey = event.altKey;
                } else {
                    matchAllKey = event.key === key;
                }
            }
        })
    } else {
        matchAllKey = false;
    }
    return matchAllKey;
};

/**
 * 字符串中间插入文本
 * @param sourceStr
 * @param start
 * @param insertStr
 * @return {*}
 */
let insertStr = (sourceStr, start, insertStr) => {
    if (!!sourceStr && !!insertStr && !isNaN(start) && sourceStr instanceof String && insertStr instanceof String) {
        return sourceStr.slice(0, start) + insertStr + sourceStr.slice(start);
    }
    return sourceStr;
};

/**
 * 数组中间插入内容
 * @param {array} sourceArr
 * @param {number} start
 * @param {*} insertObj
 * @return {*}
 */
let insertArr = (sourceArr, start, insertObj) => {
    if (!!sourceArr && !!insertObj) {
        sourceArr = _.clone(sourceArr);
        if (sourceArr instanceof Array) {
            // 判断超越最大下标
            if (sourceArr[start] === undefined) {
                sourceArr.push(insertObj);
            } else {
                // 中间添加一行
                sourceArr = sourceArr.map((v, i) => i === start ? [v, insertObj] : v).flat();
            }
        }
    }
    return sourceArr;
};

/**
 * 数组中间插入内容
 * @param {array} sourceArr
 * @param {number} start
 * @return {*}
 */
let removeArr = (sourceArr, start) => {
    if (!!sourceArr && typeof start === 'number') {
        sourceArr = _.clone(sourceArr);
        if (sourceArr instanceof Array) {
            // 中间删除一行
            sourceArr = sourceArr.filter((url, index) => index !== start);
        }
    } else {
        console.log('移除数组错误! 参数错误 sourceArr: %o start: %o', sourceArr, start);
    }
    return sourceArr;
};

/**
 * 字节转字符串
 * @param byteSize
 * @returns {string}
 */
let byteUnit = (byteSize) => {
    if (!!Math.floor(byteSize)) {
        const KB = 1024;
        const MB = 1024 * KB;
        const GB = 1024 * MB;
        let timeStr = byteSize > GB ? byteSize / GB + 'GB' : byteSize > MB ? byteSize / MB + 'MB' : byteSize > KB ? byteSize / KB + 'KB' : byteSize + 'B';
        let unit = timeStr.substr(-1);
        let unitHigh = timeStr.substr(-2, 1);
        unit = unitHigh > '9' ? unitHigh + unit : unit;
        let number = timeStr.replace(unit, '');
        number = Math.floor(number * 10) / 10;
        timeStr = number + unit;
        return timeStr;
    } else {
        return '0B'
    }
};

/**
 * 判断是否管理员
 * @return {boolean}
 */
let isAdmin = () => !!localStorage.getItem('token');

/**
 * 判断字符串是否为json
 * @param str
 * @returns {boolean}
 * @see <a href="https://www.cnblogs.com/lanleiming/p/7096973.html">【最简单的方法】js判断字符串是否为JSON格式（20180115更新）</a>
 */
let isJson = (str) => {
    if (typeof str === 'string') {
        try {
            let obj = JSON.parse(str);
            return !!(typeof obj === 'object' && obj);
        } catch (e) {
            console.log('isJson error： %o !!!', str, e);
        }
    }
    return false;
};

/**
 * 提取日志打印FormData信息
 * @param params 请求参数
 * @returns {object}
 */
let extractFormData = (params) => FormData && params && params instanceof FormData ? (() => {
    let next, returnObj = {}, pk = params.keys();
    while ((next = pk.next()) && !next.done) returnObj[next.value] = params.getAll(next.value);
    return returnObj;
})() : params;

//console.__proto__.blue = {
//    log: (...e) => console.log('%c[ %s ] Proxy' + e.map((e) => typeof(e) === 'object' ? '%o' : '%s').join(' ')
//        , 'background-color:blue;color:white;line-height:20px;', new Date().fmt(), ...e)
//};
//
//console.__proto__.green = {
//    log: (...e) => console.log('%c[ %s ] Proxy' + e.map((e) => typeof(e) === 'object' ? '%o' : '%s').join(' ')
//        , 'background-color:blue;color:blue;line-height:20px;', new Date().fmt(), ...e)
//};
//
//console.__proto__.yellow = {
//    log: (...e) => console.log('%c[ %s ] Proxy' + e.map((e) => typeof(e) === 'object' ? '%o' : '%s').join(' ')
//        , 'background-color:blue;color:yellow;line-height:20px;', new Date().fmt(), ...e)
//};
//
//console.__proto__.green = {
//    log: (...e) => console.log('%c[ %s ] Proxy' + e.map((e) => typeof(e) === 'object' ? '%o' : '%s').join(' ')
//        , 'background-color:blue;color:green;line-height:20px;', new Date().fmt(), ...e)
//};

/**
 * 编码转换
 *
 * 使用示例:
 * let str1 = charConvert('{"code":0,"msg":"æ\x93\x8Dä½\x9Cæ\x88\x90å\x8A\x9F","data":[]}', 'latin1', 'utf8')
 * let str1 = charConvert(str1,'utf-8','latin1')
 *
 * https://github.com/polygonplanet/encoding.js
 * https://unpkg.com/encoding-japanese@2.0.0/encoding.min.js
 *
 * @param str 字符串
 * @param from 原编码
 * @param to 目标编码
 * @returns {string|*} 转换后的字符串
 */
let charConvert = (str, from, to) => {
    if (typeof str !== 'string') return str;
    if (typeof Encoding === 'undefined') {
        console.log('encoding.js is not found!');
        return str;
    }
    let arrFrom = Encoding.stringToCode(str, from);
    let arrTo = Encoding.convert(arrFrom, from, to);
    return Encoding.codeToString(arrTo, to);
};

/**
 * Blob转换为base64互相转换
 * Blob序列化, 反序列化工具
 *
 * @see <a href="https://github.com/angadn/blob64">blob64</a>
 * @type {{serialize(*, *): void, deserialize(*): Blob}}
 */
let blob64 = {
    /**
     * 序列化
     * @param blob  Blob对象
     * @param onFinish 完成回调
     */
    serialize(blob, onFinish) {
        let reader = new FileReader();
        reader.readAsDataURL(blob);
        reader.onloadend = function () {
            onFinish(reader.result);
        };
    },
    /**
     * 序列化 Promise版
     * 推荐使用
     *
     * @param blob  Blob对象
     * @returns {Promise<String>} 序列化后的字符串
     */
    serializePromise(blob) {
        return new Promise(resolve => {
            this.serialize(blob, resolve);
        });
    },
    /**
     * 反序列化
     * @param str base64字符串
     * @returns {Blob} Blob对象
     */
    deserialize(str) {
        let indexOfComma = str.lastIndexOf(",");
        let contentType = str.substr(0, indexOfComma + 1) || "";
        let byteChars = atob(str.substr(indexOfComma + 1));
        let sliceSize = 512;
        let byteArrays = [];
        for (let offset = 0; offset < byteChars.length; offset += sliceSize) {
            let slice = byteChars.slice(offset, offset + sliceSize);
            let byteNumbers = new Array(slice.length);
            for (let i = 0; i < slice.length; i++) {
                byteNumbers[i] = slice.charCodeAt(i);
            }
            byteArrays.push(new Uint8Array(byteNumbers));
        }
        return new Blob(byteArrays, {type: contentType});
    },
    /**
     * 反序列化 Promise版
     * 推荐使用
     *
     * @see <a href="https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript">stackoverflow</a>  * @param str base64字符串
     * @returns {Blob} Blob对象
     */
    deserializePromise: (str) => fetch(str).then(res => res.blob()),
    /**
     * 文本转换为Blob对象
     *
     * 常用: text/plain, text/html, text/html;charset=charset=utf-8;,
     * application/octet-stream, text/css, text/javascript, application/javascript,
     * application/json, application/xml, application/x-javascript, text/xml,
     * application/xhtml+xml, application/x-www-form-urlencoded, multipart/form-data
     *
     * @see <a href="https://github.com/kevin-roark/get-text-blob">get-text-blob</a>
     * @param text 文本
     * @param mimeType MIME类型, 默认text/plain
     * @returns {null} Blob对象
     */
    textToBlob(text, mimeType = 'text/plain') {
        let blob = null;
        try {
            if (text.startsWith('data:')) {
                blob = this.deserialize(text);
            } else {
                blob = new Blob([text], {type: mimeType});
            }
        } catch (e) {
            // TypeError old chrome and FF
            let blobBuilder = window['BlobBuilder'] ||
                window['WebKitBlobBuilder'] ||
                window['MozBlobBuilder'] ||
                window['MSBlobBuilder'];
            if (blobBuilder) {
                let bb = new blobBuilder();
                bb.append([text]);
                blob = bb['getBlob']('text/html');
            }
        }
        return blob;
    },
    /**
     * Blob对象转换为文本
     *
     * @see <a href="https://blog.csdn.net/qq_46090071/article/details/124199827">js在blob对象中读取文字内容</a>
     * @param blob Blob对象
     * @param charSet 字符集
     * @returns {Promise<String>} 文本
     */
    blobToTextPromise(blob, charSet = null) {
        return new Promise(resolve => {
            let reader = new FileReader();
            reader.onloadend = function () {
                resolve(reader.result);
            };
            if (charSet) {
                reader.readAsText(blob, charSet);
            } else {
                reader.readAsText(blob);
            }
        });
    },
    /**
     * 下载Blob对象到文件
     *
     * @see <a href="https://pixijs.io/examples/#/demos-advanced/screenshot.js">screenshot</a>
     * @param blob Blob对象
     * @param fileName 文件名
     */
    downLoadBlob(blob, fileName) {
        let a = document.createElement('a');
        a.href = window.URL.createObjectURL(blob);
        a.download = fileName;
        a.click();
        window.URL.revokeObjectURL(a.href);
    },
    /**
     * 下载Blob对象到文件
     *
     * @see <a href="https://pixijs.io/examples/#/demos-advanced/screenshot.js">screenshot</a>
     * @param blob Blob对象
     * @param fileName 文件名
     */
    downBlob(blob, fileName = "download") {
        const a = document.createElement('a');
        document.body.append(a);
        a.download = fileName;
        a.href = URL.createObjectURL(blob);
        a.click();
        a.remove();
        window.URL.revokeObjectURL(a.href);
    },
}

/**
 * 禁用调试模式
 * @see <a href="https://zmingcx.com/wp-content/cache/autoptimize/js/autoptimize_e7a8a795e88cf81ddfa417af57368243.js">提取自源码forbidDebug()</a>
 */
let forbidDebug = function () {
    // 是否允许调试
    let enableDebug = true;
    if (enableDebug) {
        return;
    }
    try {
        ((function () {
            let callbacks = [], timeLimit = 50, open = false;
            setInterval(loop, 1);
            return {
                addListener: function (fn) {
                    callbacks.push(fn)
                }, cancleListenr: function (fn) {
                    callbacks = callbacks.filter(function (v) {
                        return v !== fn
                    })
                }
            };

            function loop() {
                let startTime = new Date();
                //de//bugger;
                if (new Date() - startTime > timeLimit) {
                    if (!open) {
                        callbacks.forEach(function (fn) {
                            fn.call(null)
                        })
                    }
                    open = true;
                    window.stop();
                    alert("\u5173\u95ed\u63a7\u5236\u53f0\u540e\u5237\u65b0\uff01");
                    document.body.innerHTML = ""
                } else {
                    open = false
                }
            }
        })()).addListener(function () {
            window.location.reload()
        })
    } catch (e) {
    }
};
forbidDebug();

/**
 * 自制禁用调试
 */
(() => {
    // 是否启用
    let enable = false;
    let c = String.fromCharCode;
    let d = [40, 40, 41, 61, 62, 123, 100, 101, 98, 117, 103, 103, 101, 114, 125, 41, 40, 41];
    try {
        enable && eval(d.map(e => c(e)).join(''));
    } catch (e) {
        console.error(e);
        window.stop();
    }
})();

let apis = {
    style: {
        getApp: {
            // url: 'http://127.0.0.1:3003/src/css/app.css',
            url: 'src/css/app.css',
            method: 'get'
        }
    },
    swagger: {
        apiDocs: {
            url: 'v3/api-docs',
            method: 'get'
        },
    },
    task: {
        getOne: {
            url: 'Task/${id}',
            method: 'get'
        },
        getAll: {
            url: 'Task/findAll',
            method: 'get'
        },
        add: {
            url: 'Task',
            method: 'post'
        },
        update: {
            url: 'Task',
            method: 'put'
        },
        delete: {
            url: 'Task/${id}',
            method: 'delete'
        },
        start: {
            url: 'Task/${id}/start',
            method: 'get'
        },
        stop: {
            url: 'Task/${id}/stop',
            method: 'get'
        },
        pause: {
            url: 'Task/${id}/pause',
            method: 'get'
        },
        resume: {
            url: 'Task/${id}/resume',
            method: 'get'
        },
        test: {
            url: 'Task/test',
            method: 'post'
        }
    },
    taskFar: {
        getOne: {
            url: 'TaskFar/${id}',
            method: 'get'
        },
        getAll: {
            url: 'TaskFar/findAll',
            method: 'get'
        },
        add: {
            url: 'TaskFar',
            method: 'post'
        },
        update: {
            url: 'TaskFar',
            method: 'put'
        },
        delete: {
            url: 'TaskFar/${id}',
            method: 'delete'
        },
        start: {
            url: 'TaskFar/${id}/start',
            method: 'get'
        },
        stop: {
            url: 'TaskFar/${id}/stop',
            method: 'get'
        },
        pause: {
            url: 'TaskFar/${id}/pause',
            method: 'get'
        },
        resume: {
            url: 'TaskFar/${id}/resume',
            method: 'get'
        },
        test: {
            url: 'TaskFar/test',
            method: 'post'
        }
    },
    proxy: {
        getOne: {
            url: 'Proxy/${id}',
            method: 'get'
        },
        getAll: {
            url: 'Proxy/findAll',
            method: 'get'
        },
        add: {
            url: 'Proxy',
            method: 'post'
        },
        update: {
            url: 'Proxy',
            method: 'put'
        },
        delete: {
            url: 'Proxy/${id}',
            method: 'delete'
        }
    },
    config: {
        getOne: {
            url: 'Config/${id}',
            method: 'get'
        },
        getAll: {
            url: 'Config/findAll',
            method: 'get'
        },
        add: {
            url: 'Config',
            method: 'post'
        },
        update: {
            url: 'Config',
            method: 'put'
        },
        delete: {
            url: 'Config/${id}',
            method: 'delete'
        }
    },
    user: {
        getOne: {
            url: 'User/${id}',
            method: 'get'
        },
        getAll: {
            url: 'User/findAll',
            method: 'get'
        },
        add: {
            url: 'User',
            method: 'post'
        },
        update: {
            url: 'User',
            method: 'put'
        },
        delete: {
            url: 'User/${id}',
            method: 'delete'
        }
    },
    notifyEmail: {
        getOne: {
            url: 'NotifyEmail/${id}',
            method: 'get'
        },
        getAll: {
            url: 'NotifyEmail/findAll',
            method: 'get'
        },
        add: {
            url: 'NotifyEmail',
            method: 'post'
        },
        update: {
            url: 'NotifyEmail',
            method: 'put'
        },
        delete: {
            url: 'NotifyEmail/${id}',
            method: 'delete'
        }
    },
    eMail: {
        send: {
            url: 'Email',
            method: 'post',
            param_example: {content: "发送一封邮件", title: "swagger测试", to: "ni81@qq.com"},
        },
    },
    ins: {
        count: {
            url: 'Ins/user/count',
            method: 'get'
        },
        current: {
            url: 'Ins/user/current',
            method: 'get'
        },
        move: {
            url: 'Ins/user/move',
            method: 'get'
        },
        next: {
            url: 'Ins/user/next',
            method: 'get'
        },
        reset: {
            url: 'Ins/user/reset',
            method: 'get'
        },
    },
    post: {
        getOne: {
            url: 'Ins/post/${id}',
            method: 'get'
        },
        getPage: {
            url: 'Ins/post',
            method: 'get'
        },
        addOrUpdate: {
            url: 'Ins/post',
            method: 'post'
        },
        delete: {
            url: 'Ins/post/${id}',
            method: 'delete'
        },
        deleteMany: {
            url: 'Ins/post/delete',
            method: 'delete'
        },
        count: {
            url: 'Ins/post/count',
            method: 'get'
        },
    },
    middle: {
        heartBeat: {
            url: 'middle/ping',
            method: 'get'
        },
        lastHeartBeat: {
            url: 'middle/lastHeart',
            method: 'get'
        },
    },
    file: {
        listJson: {
            url: 'file/json',
            method: 'get'
        },
        list: {
            url: 'file',
            method: 'get'
        },
        delete: {
            // file?del=<String fileName>
            url: 'file?del=${file}',
            method: 'get',
            param_example: {file: 'aaa.txt0.8008880603584201'}
        },
        getOne: {
            // file?get=<String fileName>
            url: 'file?get=${file}',
            method: 'get',
            param_example: {file: 'aaa.txt0.8008880603584201'},
            config: {
                responseType: 'blob'
            },
        },
        upload: {
            url: 'file',
            method: 'post',
            param_example: '/*FormData*/tmp=await api.file.getOne({file:\'aaa.jpg\'});fd=new FormData();fd.append(\'file\',tmp.data,\'test.jpg\'+Math.random());up=await api.file.upload(fd)',
            config: {
                overrideMimeType: 'multipart/form-data'
            },
            info: '参数格式为 FormData'
        },
    },
    gram: {
        // https://www.instagram.com/graphql/query/?query_id=17888483320059182&id=1001283596&first=40
        getPost: {
            url: 'https://www.instagram.com/graphql/query/?query_id=17888483320059182&id=${id}&first=40',
            method: 'get',
            param_example: {id: '1001283596'}
        },
        // https://www.instagram.com/graphql/query/?query_hash=8c2a529969ee035a5063f2fc8602a0fd&variables={"id":"51132914782","first":12}
        getPostNew: {
            url: 'https://www.instagram.com/graphql/query/?query_hash=${queryHash}&variables={"id":"${id}","first":40}',
            method: 'get',
            queryHash: true,
            // 多图id: '51132914782'
            // 多图__typename: 'GraphSidecar'
            param_example: {id: '1001283596'}
        },
        getJpeg: {
            url: 'http${url}',
            method: 'get',
            param_example: {url: '1001283596'},
            usage_example: 'await api.gram.getJpeg({url:decodeURIComponent(\'https://scontent-hkt1-1.cdninstagram.com/v/t51.2885-15/sh0.08/e35/c0.180.1440.1440a/s640x640/271796139_918243432220319_6606270248120166088_n.jpg?_nc_ht=scontent-hkt1-1.cdninstagram.com\u0026_nc_cat=111\u0026_nc_ohc=2KPbEtGsfIIAX9MVvN3\u0026edm=APU89FABAAAA\u0026ccb=7-4\u0026oh=00_AT9LlzKJwZxDGjaZyEI4ov4XEGonwuvdU0xr2oAZ4kCgWA\u0026oe=61EC5D1C\u0026_nc_sid=86f79a\').substr(4)})',
            config: {
                responseType: 'blob'
            },
        },
    },
    far: {
        getTest: {
            url: '/hk/shopping/women/dion-lee-corset-detail-minidress-item-17005926.aspx',
            method: 'get',
            example: `.between('content="',' minidress',true)==='Shop Dion Lee corset-detail'`
        },
    },
    mongo: {
        save: {
            url: 'Mongo/post/${name}',
            method: 'post',
            example: `{id: '1001283596'}`,
            param_example: {id: '1001283596', name: 'test'}
        },
        saveMany: {
            url: 'Mongo/post/${name}/many',
            method: 'post',
            example: `{id: '1001283596'}`,
            param_example: {id: '1001283596', name: 'test'}
        },
        updateMany: {
            url: 'Mongo/post/${name}/many',
            method: 'put',
            example: `{condition: {id: '1001283596'}, update: {id: '1001283596'}}`,
            param_example: {condition: {id: '1001283596'}, update: {id: '1001283596', name: 'test'}}
        },
        delete: {
            url: 'Mongo/post/${name}/${id}',
            method: 'delete',
            param_example: {name: 'test', id: '1001283596'}
        },
        findOne: {
            url: '/Mongo/post/${name}/${id}',
            method: 'get',
            param_example: {
                name: 'test', id: '1001283596'
            }
        },
        find: {
            url: 'Mongo/post/${name}/${start}/${size}',
            method: 'post',
            param_example: {
                name: 'test', start: 0, size: 10,
                id: '1001283596'
            }
        },
        count: {
            url: 'Mongo/post/${name}/count',
            method: 'get',
            param_example: {name: 'test'}
        },
        countCondition: {
            url: 'Mongo/post/${name}/count',
            method: 'post',
            param_example: {name: 'test', id: '1001283596'}
        },
    }
};

let doBuildUrl = (apiCfg, params) => {
    let url = apiCfg.url.startsWith('http') ? apiCfg.url : appConfig.host + apiCfg.url;
    let pathParams = url.match(/\${\w+}/g);
    if (pathParams) {
        pathParams.forEach(param => {
            let paramKey = param.match(/\${(.*?)}/)[1];
            let paramValue = params[paramKey] || '';
            // 数组参数使用方式: 第一条数据作为路径参数, 第二条数据作为参数; 例如: /api/post/1001283596/many
            // 没有路径参数的话, 全部作为参数; 例如: /api/post/many
            if (Array.isArray(params) && params.length > 0) {
                let paramObj = params[0];
                paramValue = paramObj[paramKey] || '';
                delete paramObj[paramKey];
            }
            if (apiCfg.queryHash && 'queryHash' === paramKey && !paramValue) {
                paramValue = store.state.queryHash;
            }
            url = url.replace(param, paramValue);
            delete params[paramKey];
        })
    }
    return url;
};

/**
 * build for axios
 * @param apiCfg
 * @returns {(function(*=): *)|*}
 */
let build = (apiCfg) => {
    if (apiCfg) {
        let keys = Object.keys(apiCfg);
        if (keys.includes('url') && apiCfg.url) {
            let method = apiCfg.method || 'get';
            axios[method].toString();
            //try {
            //} catch (e) {
            //    throw new Error('错误的方法名! method: ' + method + ' url: ' + apiCfg.url);
            //}
            return (originParams = {}) => {
                let params = deepClone(originParams);
                let url = doBuildUrl(apiCfg, params);
                // 支持配置信息(如文件下载 blob)
                if (apiCfg.config) {
                    Object.assign(params, apiCfg.config)
                }
                // 支持表单提交(如文件上传 FormData)
                if (typeof (originParams) && originParams instanceof FormData) {
                    if (appConfig.isDebug) {
                        console.log('url', url, extractFormData(originParams), params, apiCfg);
                    }
                    return axios[method](url, originParams, params);
                }
                if (appConfig.isDebug) {
                    console.log('url', url, extractFormData(params), apiCfg);
                }
                // 数组参数使用方式: 第一条数据作为路径参数, 第二条数据作为参数; 例如: /api/post/1001283596/many
                // 没有路径参数的话, 全部作为参数; 例如: /api/post/many
                if (Array.isArray(params) && params.length > 0) {
                    params = params[1] ? params[1] : [];
                }
                return axios[method](url, params);
            }
        } else {
            keys.forEach(key => apiCfg[key] = build(apiCfg[key]));
            return apiCfg;
        }
    }
};

/**
 * build for GM_xmlhttpRequest
 * @param apiCfg
 * @returns {(function(*=): *)|*}
 */
let buildGm = (apiCfg) => {
    if (apiCfg) {
        let keys = Object.keys(apiCfg);
        if (keys.includes('url') && apiCfg.url) {
            let method = apiCfg.method || 'get';
            return (params = {}) => {
                params = deepClone(params);
                let url = doBuildUrl(apiCfg, params);
                // 数组参数使用方式: 第一条数据作为路径参数, 第二条数据作为参数; 例如: /api/post/1001283596/many
                // 没有路径参数的话, 全部作为参数; 例如: /api/post/many
                if (Array.isArray(params) && params.length > 0) {
                    params = params[1] ? params[1] : [];
                }
                let param = {
                    url,
                    method,
                    data: JSON.stringify(params),
                };
                console.log('url', url, params, apiCfg, param);
                window.GM_xmlhttpRequest = typeof (GM_xmlhttpRequest) === 'undefined' ? undefined : GM_xmlhttpRequest;
                return new Promise((resolve, reject) => {
                    param.onerror = (error) => {
                        reject(error)
                    };
                    param.onload = (resp) => {
                        let headers = {};
                        let headArr = resp.responseHeaders.split("\n");
                        headArr.forEach(head => {
                            let hArr = head.split(";");
                            let key = hArr.shift();
                            headers[key] = hArr.join().trim();
                        });
                        resp.headers = headers;
                        let data = {};
                        if (isJson(resp.responseText)) {
                            data = JSON.parse(resp.responseText);
                        }
                        resp.data = data;
                        resolve(resp);
                    };
                    if (!!GM_xmlhttpRequest) {
                        GM_xmlhttpRequest(param)
                    } else {
                        console.error("没有找到GM_xmlhttpRequest");
                    }
                });
            }
        } else {
            keys.forEach(key => apiCfg[key] = buildGm(apiCfg[key]));
            return apiCfg;
        }
    }
};

/**
 * usage in tamperMonkey:
 * let gmDownFile = buildGmDownFile();
 *
 * @returns {function(String): Promise<Object>}
 * @see <a href="https://www.tampermonkey.net/documentation.php#GM_xmlhttpRequest">tamperMonkey doc</a>
 * @author http://mmbro.gitee.com
 * @since 2022110
 */
let buildGmDownFile = () => {
    window.GM_xmlhttpRequest = typeof (GM_xmlhttpRequest) === 'undefined' ? undefined : GM_xmlhttpRequest;
    return (url) => {
        url = String(url).startsWith('http') ? url : appConfig.host + url;
        return new Promise((resolve, reject) => {
            let param = {
                url,
                method: 'get',
                responseType: 'blob'
            };
            param.onerror = (error) => {
                reject(error)
            };
            param.onload = (resp) => {
                resp.data = resp.response;
                resolve(resp);
            };
            if (!!GM_xmlhttpRequest) {
                GM_xmlhttpRequest(param)
            } else {
                console.error("没有找到GM_xmlhttpRequest");
            }
        })
    }
};

/**
 * usage in tamperMonkey:
 * let gmUpLoad = buildGmUpLoadFile();
 *
 * @returns {function(String, FormData): Promise<Object>}
 * @see <a href="https://www.tampermonkey.net/documentation.php#GM_xmlhttpRequest">tamperMonkey doc</a>
 * @author http://mmbro.gitee.com
 * @since 2022110
 */
let buildGmUpLoadFile = () => {
    window.GM_xmlhttpRequest = typeof (GM_xmlhttpRequest) === 'undefined' ? undefined : GM_xmlhttpRequest;
    return (url, formData) => {
        url = String(url).startsWith('http') ? url : appConfig.host + url;
        return new Promise((resolve, reject) => {
            let param = {
                url,
                data: formData,
                method: 'post',
                responseType: 'blob',
                overrideMimeType: 'multipart/form-data',
            };
            if (typeof (formData) === 'undefined') {
                reject('error! formData is not a FormData Object!')
            }
            param.onerror = (error) => {
                reject(error)
            };
            let isHeadersJson = (headers) => headers && headers['content-type'] && String(headers['content-type']).indexOf('application/json') >= 0;
            param.onload = (resp) => {
                let extractRespHeaders = (responseHeaders) => {
                    let headers = {};
                    let headArr = responseHeaders.split("\n");
                    headArr.forEach(head => {
                        if (head) {
                            let hArr = head.split(":");
                            let key = hArr.shift();
                            headers[key] = hArr.join().trim();
                        }
                    });
                    return headers;
                };
                resp.headers = extractRespHeaders(resp.responseHeaders);
                let data;
                if (isHeadersJson(headers)) {
                    if (isJson(resp.responseText)) {
                        data = JSON.parse(resp.responseText);
                    } else {
                        if (appConfig.isDebug) {
                            let contentType = headers['content-type'];
                            console.error('响应非json文本,contentType: %s', contentType);
                        }
                        data = resp.responseText;
                    }
                } else {
                    data = resp.responseText;
                }
                resp.data = data;
                resp.request = param;
                resolve(resp);
            };
            if (!!GM_xmlhttpRequest) {
                GM_xmlhttpRequest(param)
            } else {
                console.error("没有找到GM_xmlhttpRequest");
            }
        })
    }
};/**
 * Vue3 简单状态管理
 * @see <a href="https://v3.cn.vuejs.org/guide/state-management.html">从零打造简单状态管理</a>
 */
let store = {
    debug: true,
    state: Vue.reactive({
        runTask: null,
        apiDocs: null,
        lastMessageTime: new Date(),
        healthState: true,
        queryHash: '',
    }),

    setRunTask(newValue) {
        if (this.debug) {
            console.log('setRunTask triggered with', newValue)
        }

        this.state.runTask = newValue
    },

    clearRunTask(newValue) {
        if (this.debug) {
            console.log('clearRunTask triggered with', newValue)
        }

        let {runTask} = this.state;
        if (newValue && runTask && runTask.id === newValue.id) {
            this.state.runTask = null
        }
    },
};
/**
 * axiosTamperMonkeyAdapter.js
 * axios GM_xmlhttpRequest adapter
 *
 * support axios version: axios@0.24.0
 * usage when loaded:
 * axios.defaults.adapter = xhrAdapter;
 * @param config
 * @returns {Promise<void>}
 * @author http://mmbro.gitee.com
 * @since 202217
 * @version 1.0.0
 */

var originAdapter = null;
if (!!axios && !!axios.defaults && !!axios.defaults.adapter) {
    originAdapter = axios.defaults.adapter;
}

// 配置依赖兼容隔离
if (typeof (appConfig) === 'undefined') {
    appConfig = {
        isDebug: true,
        axiosTimeOut: 60 * 1000,
    };
    if (typeof (unsafeWindow) !== 'undefined') {
        console.log('unsafeWindow is undefined, set up appConfig');
        unsafeWindow.appConfig = appConfig;
    }
}

// 配置依赖兼容隔离
if (typeof (isJson) === 'undefined') {
    /**
     * 判断字符串是否为json(xhrAdapter依赖)
     * @param str
     * @returns {boolean}
     * @see <a href="https://www.cnblogs.com/lanleiming/p/7096973.html">【最简单的方法】js判断字符串是否为JSON格式（20180115更新）</a>
     */
    window.isJson = (str) => {
        if (typeof str === 'string') {
            try {
                let obj = JSON.parse(str);
                return !!(typeof obj === 'object' && obj);
            } catch (e) {
                console.log('isJson error： %o !!!', str, e);
            }
        }
        return false;
    };
}

function xhrAdapter(config) {
    // before keep no change for axios origin code.
    return new Promise(function dispatchXhrRequest(resolve, reject) {
        var requestData = config.data;
        var requestHeaders = config.headers;
        var responseType = config.responseType;
        var overrideMimeType = config.overrideMimeType;
        var onCanceled;

        // 全局超时设置
        if (!config.timeout) {
            config.timeout = appConfig.axiosTimeOut;
            if (appConfig.isDebug) {
                console.debug(`超时全局配置=${config.timeout}`);
            }
        }
        // var request = new XMLHttpRequest();
        //
        // // HTTP basic authentication
        // if (config.auth) {
        //     var username = config.auth.username || '';
        //     var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
        //     requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
        // }
        //
        // var fullPath = buildFullPath(config.baseURL, config.url);
        // request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
        //
        // // Set the request timeout in MS
        // request.timeout = config.timeout;
        //
        // function onloadend() {
        //     if (!request) {
        //         return;
        //     }
        //     // Prepare the response
        //     var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
        //     var responseData = !responseType || responseType === 'text' || responseType === 'json' ?
        //         request.responseText : request.response;
        //     var response = {
        //         data: responseData,
        //         status: request.status,
        //         statusText: request.statusText,
        //         headers: responseHeaders,
        //         config: config,
        //         request: request
        //     };
        //
        //     settle(function _resolve(value) {
        //         resolve(value);
        //         done();
        //     }, function _reject(err) {
        //         reject(err);
        //         done();
        //     }, response);
        //
        //     // Clean up request
        //     request = null;
        // }
        //
        // if ('onloadend' in request) {
        //     // Use onloadend if available
        //     request.onloadend = onloadend;
        // } else {
        //     // Listen for ready state to emulate onloadend
        //     request.onreadystatechange = function handleLoad() {
        //         if (!request || request.readyState !== 4) {
        //             return;
        //         }
        //
        //         // The request errored out and we didn't get a response, this will be
        //         // handled by onerror instead
        //         // With one exception: request that using file: protocol, most browsers
        //         // will return status as 0 even though it's a successful request
        //         if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        //             return;
        //         }
        //         // readystate handler is calling before onerror or ontimeout handlers,
        //         // so we should call onloadend on the next 'tick'
        //         setTimeout(onloadend);
        //     };
        // }
        //
        // // Handle browser request cancellation (as opposed to a manual cancellation)
        // request.onabort = function handleAbort() {
        //     if (!request) {
        //         return;
        //     }
        //
        //     reject(createError('Request aborted', config, 'ECONNABORTED', request));
        //
        //     // Clean up request
        //     request = null;
        // };
        //
        // // Handle low level network errors
        // request.onerror = function handleError() {
        //     // Real errors are hidden from us by the browser
        //     // onerror should only fire if it's a network error
        //     reject(createError('Network Error', config, null, request));
        //
        //     // Clean up request
        //     request = null;
        // };
        //
        // // Handle timeout
        // request.ontimeout = function handleTimeout() {
        //     var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
        //     var transitional = config.transitional || defaults.transitional;
        //     if (config.timeoutErrorMessage) {
        //         timeoutErrorMessage = config.timeoutErrorMessage;
        //     }
        //     reject(createError(
        //         timeoutErrorMessage,
        //         config,
        //         transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
        //         request));
        //
        //     // Clean up request
        //     request = null;
        // };
        //
        // // Add xsrf header
        // // This is only done if running in a standard browser environment.
        // // Specifically not if we're in a web worker, or react-native.
        // if (utils.isStandardBrowserEnv()) {
        //     // Add xsrf header
        //     var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        //         cookies.read(config.xsrfCookieName) :
        //         undefined;
        //
        //     if (xsrfValue) {
        //         requestHeaders[config.xsrfHeaderName] = xsrfValue;
        //     }
        // }
        //
        // // Add headers to the request
        // if ('setRequestHeader' in request) {
        //     utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        //         if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
        //             // Remove Content-Type if data is undefined
        //             delete requestHeaders[key];
        //         } else {
        //             // Otherwise add header to the request
        //             request.setRequestHeader(key, val);
        //         }
        //     });
        // }
        //
        // // Add withCredentials to request if needed
        // if (!utils.isUndefined(config.withCredentials)) {
        //     request.withCredentials = !!config.withCredentials;
        // }
        //
        // // Add responseType to request if needed
        // if (responseType && responseType !== 'json') {
        //     request.responseType = config.responseType;
        // }
        //
        // // Handle progress if needed
        // if (typeof config.onDownloadProgress === 'function') {
        //     request.addEventListener('progress', config.onDownloadProgress);
        // }
        //
        // // Not all browsers support upload events
        // if (typeof config.onUploadProgress === 'function' && request.upload) {
        //     request.upload.addEventListener('progress', config.onUploadProgress);
        // }
        //
        // if ('onloadend' in request) {
        //     // Use onloadend if available
        //     request.onloadend = onloadend;
        // } else {
        //     // Listen for ready state to emulate onloadend
        //     request.onreadystatechange = function handleLoad() {
        //         if (!request || request.readyState !== 4) {
        //             return;
        //         }
        //
        //         // The request errored out and we didn't get a response, this will be
        //         // handled by onerror instead
        //         // With one exception: request that using file: protocol, most browsers
        //         // will return status as 0 even though it's a successful request
        //         if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        //             return;
        //         }
        //         // readystate handler is calling before onerror or ontimeout handlers,
        //         // so we should call onloadend on the next 'tick'
        //         setTimeout(onloadend);
        //     };
        // }
        //
        // // Handle browser request cancellation (as opposed to a manual cancellation)
        // request.onabort = function handleAbort() {
        //     if (!request) {
        //         return;
        //     }
        //
        //     reject(createError('Request aborted', config, 'ECONNABORTED', request));
        //
        //     // Clean up request
        //     request = null;
        // };
        //
        // // Handle low level network errors
        // request.onerror = function handleError() {
        //     // Real errors are hidden from us by the browser
        //     // onerror should only fire if it's a network error
        //     reject(createError('Network Error', config, null, request));
        //
        //     // Clean up request
        //     request = null;
        // };
        //
        // // Handle timeout
        // request.ontimeout = function handleTimeout() {
        //     var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
        //     var transitional = config.transitional || defaults.transitional;
        //     if (config.timeoutErrorMessage) {
        //         timeoutErrorMessage = config.timeoutErrorMessage;
        //     }
        //     reject(createError(
        //         timeoutErrorMessage,
        //         config,
        //         transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
        //         request));
        //
        //     // Clean up request
        //     request = null;
        // };
        //
        // // Add xsrf header
        // // This is only done if running in a standard browser environment.
        // // Specifically not if we're in a web worker, or react-native.
        // if (utils.isStandardBrowserEnv()) {
        //     // Add xsrf header
        //     var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        //         cookies.read(config.xsrfCookieName) :
        //         undefined;
        //
        //     if (xsrfValue) {
        //         requestHeaders[config.xsrfHeaderName] = xsrfValue;
        //     }
        // }
        //
        // // Add headers to the request
        // if ('setRequestHeader' in request) {
        //     utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        //         if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
        //             // Remove Content-Type if data is undefined
        //             delete requestHeaders[key];
        //         } else {
        //             // Otherwise add header to the request
        //             request.setRequestHeader(key, val);
        //         }
        //     });
        // }
        //
        // // Add withCredentials to request if needed
        // if (!utils.isUndefined(config.withCredentials)) {
        //     request.withCredentials = !!config.withCredentials;
        // }
        //
        // // Add responseType to request if needed
        // if (responseType && responseType !== 'json') {
        //     request.responseType = config.responseType;
        // }
        //
        // // Handle progress if needed
        // if (typeof config.onDownloadProgress === 'function') {
        //     request.addEventListener('progress', config.onDownloadProgress);
        // }
        //
        // // Not all browsers support upload events
        // if (typeof config.onUploadProgress === 'function' && request.upload) {
        //     request.upload.addEventListener('progress', config.onUploadProgress);
        // }
        //
        // if (config.cancelToken || config.signal) {
        //     // Handle cancellation
        //     // eslint-disable-next-line func-names
        //     onCanceled = function (cancel) {
        //         if (!request) {
        //             return;
        //         }
        //         reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
        //         request.abort();
        //         request = null;
        //     };
        //
        //     config.cancelToken && config.cancelToken.subscribe(onCanceled);
        //     if (config.signal) {
        //         config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
        //     }
        // }
        //
        // if (!requestData) {
        //     requestData = null;
        // }

        let isHeadersJson = (headers) => headers && headers['content-type'] && String(headers['content-type']).indexOf('application/json') >= 0;

        if (typeof (GM_xmlhttpRequest) === 'undefined') {
            // use axios original code with noChange.
            // 使用axios原来方式调用
            // Send the request
            if (typeof (appConfig) !== 'undefined' && appConfig.isDebug) {
                console.log('use origin axios adapter; config: ', config);
            }
            originAdapter(config).then(suc => resolve(suc)).catch(err => reject(err));
            // resolve(requestData);
        } else {
            // after now, use GM_xmlhttpRequest to adapter axios request
            // 使用GM_xmlhttpRequest方式调用
            if (typeof (appConfig) !== 'undefined' && appConfig.isDebug) {
                console.log('use new axios adapter; config: ', config);
            }
            window.GM_xmlhttpRequest = typeof (GM_xmlhttpRequest) === 'undefined' ? undefined : GM_xmlhttpRequest;
            let url = config.url;
            let method = config.method.toUpperCase();
            let data = requestData;
            let headers = config.headers;
            if (!data) {
                data = null;
            } else {
                if (isHeadersJson(headers)) {
                    if (isJson(data)) {
                        data = JSON.parse(data);
                    }
                } else {
                    if (typeof (data) === 'undefined') {
                        data = null;
                    } else if (typeof (data) !== 'string') {
                        // 添加FormData支持
                        if (data instanceof FormData) {
                            // @see <a href="https://blog.csdn.net/weixin_34413802/article/details/88722992">前端通过axios和FormData实现文件上传功能遇到的坑</a>
                            // 让浏览器来继续设置这个值
                            delete headers['content-type'];
                        } else {
                            data = JSON.stringify(data);
                        }
                    }
                }
            }
            let param = {
                url,
                method,
                headers,
                data,
                timeout: config.timeout,
            };
            if (responseType) {
                // 支持blob下载
                param.responseType = responseType;
            }
            if (overrideMimeType) {
                // 支持form表单提交
                if (appConfig.isDebug) {
                    console.log('支持form表单提交');
                }
                param.overrideMimeType = overrideMimeType;
            }
            param.onerror = (error) => {
                reject(error)
            };
            param.ontimeout = (timeout) => {
                console.error('execute timeout! config=%o, timeout=%o', config, timeout);
                reject(timeout);
            };
            param.onload = (resp) => {
                let headers = {};
                let headArr = resp.responseHeaders.split("\n");
                headArr.forEach(head => {
                    if (head) {
                        let hArr = head.split(":");
                        let key = hArr.shift();
                        headers[key] = hArr.join().trim();
                    }
                });
                resp.headers = headers;
                let data;
                if (isHeadersJson(headers)) {
                    if (isJson(resp.responseText)) {
                        data = JSON.parse(resp.responseText);
                    } else {
                        if (typeof (appConfig) !== 'undefined' && appConfig.isDebug) {
                            let contentType = headers['content-type'];
                            console.error('响应非json文本,contentType: %s', contentType);
                        }
                        data = resp.responseText;
                    }
                } else {
                    // 支持 blob, json 请求
                    if (responseType && (responseType === 'blob' || responseType === 'json')) {
                        data = resp.response;
                    } else {
                        data = resp.responseText;
                    }
                }
                resp.data = data;
                resp.config = config;
                resp.request = param;

                // 参考 axios ["data", "status", "statusText", "headers", "config", "request"]
                // // Prepare the response
                // var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
                // var responseData = !responseType || responseType === 'text' || responseType === 'json' ?
                //     request.responseText : request.response;
                // var response = {
                //     data: responseData,
                //     status: request.status,
                //     statusText: request.statusText,
                //     headers: responseHeaders,
                //     config: config,
                //     request: request
                // };
                resolve(resp);
            };
            if (!!GM_xmlhttpRequest) {
                GM_xmlhttpRequest(param)
            } else {
                console.error("没有找到GM_xmlhttpRequest");
            }
        }
    });
};
var template = `
<div class="list">
    <table v-if="list.length > 0 && Object.keys(showModel).length > 0" border="0" cellspacing="0" cellpadding="5">
        <tr><th v-for="key in Object.keys(showModel)">{{doGetKey(key)}}</th><th v-if="controlSlot">操作</th></tr>
        <tr :class="{green: doSuccess(item)}" v-for="item in list">
            <td v-for="key in Object.keys(showModel)">{{doFormat(item,key)}}</td>
            <td v-if="controlSlot"><slot :item="item"></slot></td>
            
        </tr>
    </table>
    <div class="no-data" v-else>没有数据</div>
</div>
`;

let List = {
    name: 'List',
    components: {},
    template,
    props: {
        list: {
            type: Array,
            default: []
        },
        showModel: {
            type: Object,
            default: {}
        },
        schema: {
            type: String,
            default: ''
        },
    },
    methods: {
        doFormat(item, key) {
            if (this.showModel[key] && this.showModel[key].formatter) {
                if ('date' === this.showModel[key].type) {
                    return new Date(item[key]).fmt(this.showModel[key].formatter);
                } else {
                    return item[key];
                }
            } else if ('text' === this.showModel[key].type) {
                if (this.showModel[key].maxLength && this.showModel[key].maxLength > 0) {
                    let maxLength = this.showModel[key].maxLength;
                    if (typeof (item[key]) === 'string' && item[key].length > maxLength) {
                        return item[key].substr(0, maxLength) + '...';
                    } else {
                        return item[key];
                    }
                } else {
                    return item[key];
                }
            } else {
                return item[key];
            }
        },
        doSuccess(item) {
            let successKeys = Object.keys(this.showModel).filter(key => !!this.showModel[key] && this.showModel[key].hasOwnProperty('success'));
            let success = successKeys.length > 0;
            successKeys.forEach(key => {
                if (item[key] !== this.showModel[key]['success']) {
                    success = false;
                }
            });
            return success;
        },
        doGetKey(key = '') {
            let {apiDocs} = this.sharedState;
            if (apiDocs) {
                let {schemas = {}} = apiDocs.components;
                let schemaObj = schemas[this.schema];
                if (schemaObj) {
                    return schemaObj.properties && schemaObj.properties[key] && schemaObj.properties[key].description || key;
                }
            }
            return key;
        },
    },
    data() {
        const {useSlots} = Vue;
        return {
            sharedState: store.state,
            controlSlot: !!useSlots().default
        }
    }
};
var template = `
<div class="form">
    <div class="prop" v-if="Object.keys(data).length > 0 && Object.keys(showModel).length > 0">
        <div v-for="key in Object.keys(showModel)">
            <div class="label"><label>{{doGetKey(key)}}:</label></div>
            <div class="input">
                <div v-if="showModel[key] && 'boolean' === showModel[key].type">
                    <input type="radio" value="true" v-model="data[key]">
                    <label>true</label>
                    <input type="radio" value="false" v-model="data[key]">
                    <label>false</label>
                </div>
                <div v-else-if="showModel[key] && 'map' === showModel[key].type">
                    <div class="map" v-for="(mValue,mKey,index) in data[key]">
                        <input class="key" type="text" :value="mKey" @input="changeKey(key,index,mKey,$event.target.value)">
                        <label> : </label>
                        <input class="value" type="text" :value="mValue" @input="changeValue(key,index,mValue,$event.target.value)">
                        <button class="del" @click="changeRemove(key,index)">-</button>
                    </div>
                    <button class="add" @click="changeAdd(key)">+</button>
                </div>
                <input v-else type="text" v-model="data[key]"/>
            </div>
            <div style="clear:both;"></div>
        </div>
        <div style="clear:both;"></div>
    </div>
    <div class="no-data" v-else>没有模型</div>
    <div class="buttons">
        <button @click="submit">提交</button>
        <button @click="cancel">取消</button>
        <div style="clear:both;"></div>
    </div>

</div>
`;


let Form = {
    name: 'Form',
    components: {},
    template,
    props: {
        data: {
            type: Object,
            default: {}
        },
        edit: {
            type: Object,
            default: {}
        },
        showModel: {
            type: Object,
            default: {}
        },
        schema: {
            type: String,
            default: ''
        },
    },
    methods: {
        submit() {
            this.$emit('submit')
        },
        cancel() {
            this.$emit('cancel')
        },
        changeKey(key, index, oldVal, newVal) {
            let map = this.data[key];
            let listElement = Object.keys(map).map(k => [k, map[k]]);
            listElement[index][0] = newVal;
            let newObj = {};
            listElement.forEach(arr => newObj[arr[0]] = arr[1]);
            this.data[key] = newObj;
        },
        changeValue(key, index, oldVal, newVal) {
            let map = this.data[key];
            let listElement = Object.keys(map).map(k => [k, map[k]]);
            listElement[index][1] = newVal;
            let newObj = {};
            listElement.forEach(arr => newObj[arr[0]] = arr[1]);
            this.data[key] = newObj;
        },
        changeRemove(key, index) {
            console.log('changeRemove');
            let map = this.data[key];
            let listElement = Object.keys(map).map(k => [k, map[k]]);
            let newObj = {};
            listElement.forEach((arr, ind) => {
                if (index !== ind) {
                    newObj[arr[0]] = arr[1];
                }
            });
            this.data[key] = newObj;
        },
        changeAdd(key) {
            let map = this.data[key];
            map[''] = '';
        },
        doGetKey(key = '') {
            let {apiDocs} = this.sharedState;
            if (apiDocs) {
                let {schemas = {}} = apiDocs.components;
                let schemaObj = schemas[this.schema];
                if (schemaObj) {
                    return schemaObj.properties && schemaObj.properties[key] && schemaObj.properties[key].description || key;
                }
            }
            return key;
        },
    },
    data() {
        return {
            sharedState: store.state,
        }
    },
};
var template = `
<div class="runner">
    <hr/>
    <div v-if="banMsgFar">{{banMsgFar}}</div>
    <div v-if="sharedState.runTask">
        runTask
        <div v-if="runTask" class="form">
            <div v-for="key in Object.keys(showModel)">
                <div class="label"><label>{{doGetKeyFar(key)}}:</label></div>
                <div class="input">{{doFormatFar(runTask,key)}}</div>
                <div style="clear:both;"></div>
            </div>
        </div>
    </div>
    <div class="no-data" v-else>没有数据</div>
</div>
`;

let RunnerFar = {
    name: 'RunnerFar',
    components: {},
    template,
    props: {},
    data() {
        return {
            sharedState: store.state,
            punishMs: appConfig.heartBeatPunish * 1000,
            lastTimeOutId: -1,
            timeoutMs: appConfig.heartBeatFixedDelay * 1000,
            lastSuccessTime: Date.now(),
            lastCostTime: -1,
            exit: false,
            sleepTimeMs: 10,
            runTask: null,
            nextList: [],
            nextModel: {
                // 下一个方法名
                name: '',
                // 下一个方法入参
                param: {}
            },
            // 任务栈计数
            nextCount: 0,
            // 任务栈空闲计数 空闲10次
            nextNothingCount: 0,
            runnerId: UUID(),
            config: {},
            isUserCheck: false,
            isUserOk: false,
            isProxyOk: false,
            // 任务是否正常
            isTaskOk: false,
            taskFailCount: 0,
            taskCheckUrl: '',
            sharedData: null,
            username: '',
            current: -1,
            currentUrl: '',
            total: -1,
            tempPost: null,
            tempPostEdges: null,
            tempPostEdgesSize: null,
            tempPostEdgesIndex: 0,
            tempPostEdgesFailCount: 0,
            tempPicImagePaths: [],
            tempPicList: [],
            ignoreGetImage: appConfig.ignoreGetImage,
            updateEdgeList: [],
            updateEdgeListIndex: -1,
            updateEdgeListSize: -1,
            schema: '任务信息',
            showModel: {
                taskName: '',
                currentUrl: '',
                // threadSize:'',
                // first:'',
                total: '',
                current: '',
                percentSuccess: '',
                // timeoutExecTime:'',
                // runningThread:'',
                running: '',
                paused: '',
                firstExecTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
                lastExecTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
                latestActiveTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
                createdTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
                updatedTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
            },
            banMsgFar: '',
            // url是否是商品详情页
            isItem: '-track-',
            dp: new DOMParser(),
            // 商品信息
            itemData: null,
        }

    },
    methods: {
        // 初始化
        doGetKeyFar(key = '') {
            let {apiDocs} = this.sharedState;
            if (apiDocs) {
                let {schemas = {}} = apiDocs.components;
                let schemaObj = schemas[this.schema];
                if (schemaObj) {
                    return schemaObj.properties && schemaObj.properties[key] && schemaObj.properties[key].description || key;
                }
            }
            return key;
        },
        // 格式化
        doFormatFar(item, key) {
            if (this.showModel[key] && this.showModel[key].formatter) {
                if ('date' === this.showModel[key].type) {
                    return new Date(item[key]).fmt(this.showModel[key].formatter);
                } else {
                    return item[key];
                }
            } else if ('text' === this.showModel[key].type) {
                if (this.showModel[key].maxLength && this.showModel[key].maxLength > 0) {
                    let maxLength = this.showModel[key].maxLength;
                    if (typeof (item[key]) === 'string' && item[key].length > maxLength) {
                        return item[key].substr(0, maxLength) + '...';
                    } else {
                        return item[key];
                    }
                } else {
                    return item[key];
                }
            } else {
                return item[key];
            }
        },
        // 格式化
        async doInitFar() {
            let {runTask} = this.sharedState;
            if (!runTask) {
                return;
            }
            this.runTask = runTask;
            let {firstExecTime = 0} = runTask;
            if (firstExecTime <= 0) {
                firstExecTime = Date.now();
                runTask.firstExecTime = firstExecTime;
            }
            runTask.lastExecTime = Date.now();
            let config = await this.doGetConfigFar();
            let total = await this.doGetTotalFar();
            let left = await this.doGetLeftFar();
            if(total > 0 && left === 0){
                // 已经没有了, 重新下载所有链接
                console.log('已经没有了, 重新下载所有链接');
                let resReset = await this.doResetFar();
                console.log('重新下载所有链接', resReset);
            }
            let current = await this.doGetCurrentFar();
            let itemTotal = await this.doGetItemTotalFar();
            this.total = total;
            runTask.total = total;
            runTask.current = current;
            runTask.percentSuccess = current / total * 100;
            runTask.itemTotal = itemTotal;
            this.config = config;
            let {isUserCheck = false} = config;
            let {sleepTaskMs} = config;
            sleepTaskMs = sleepTaskMs > 0 ? sleepTaskMs : appConfig.defaultSleepTaskMs;
            let lastTaskMs = localStorage.getItem("lastTaskMs");
            if (lastTaskMs && Date.now() - Number(lastTaskMs) < sleepTaskMs) {
                let msg = ['任务启动失败!', `上次任务过去时间不够久 ${new Date(Number(lastTaskMs)).fmt()}`];
                console.warn(...msg);
                this.$message({message: msg});
                this.doStopFar();
                return;
            }
            this.isUserCheck = isUserCheck;
            if (!isUserCheck) {
                this.isUserOk = true;
            }
            let {sleepTimeMs = 500} = config;
            this.sleepTimeMs = sleepTimeMs;
            this.timeoutMs = sleepTimeMs;
            return this.doStartFar();
        },
        // 获取配置
        async doStartFar() {
            let {runTask} = this.sharedState;
            if (!runTask) {
                let msg = ['任务启动异常!', 'runTask:' + typeof (runTask)];
                console.error(...msg);
                this.$message({message: msg, level: 'error'});
                this.doStopFar();
                return;
            }
            let {taskName = ''} = runTask;
            this.$message({message: ['任务开始', taskName]});
            let startCount = 0;
            while (!this.exit) {
                if (this.sleepTimeMs <= 100) {
                    this.sleepTimeMs = 100;
                }
                await this.sleepFar(this.sleepTimeMs);
                // ------
                // ------
                // ------ 局部函数 ------
                let isPausedFar = async () => {
                    let start = Date.now();
                    while (runTask.paused && runTask.running) {
                        await this.sleepFar(this.sleepTimeMs);
                        let past = Date.now() - start;
                        if (past / 1000 > 30) {
                            let msg = [`任务暂停中 ${this.runnerId}`, `current=${this.current} cid=${this.currentUrl}`];
                            console.log(...msg);
                            this.$message({message: msg});
                            start = Date.now();
                        }
                    }
                };
                let isFinishedFar = () => this.current + 1 === this.total && this.total > 0;
                let isRunningFar = () => runTask.running && !appConfig.allStop;
                let isTaskIntervalFar = () => 0;
                // ------
                // ------
                // ========
                // ========
                // ======== 任务执行
                // ========
                // ========
                // ========
                // ========
                if (isFinishedFar()) {
                    localStorage.setItem("lastTaskMs", String(Date.now()));
                    this.$message();
                    this.doStopFar();
                    this.exit = true;
                    break;
                }
                // =====  检查用户
                if (this.isUserCheck) {
                    /*
                    检查用户登录
                     */
                }
                // ====== 获取登录信息
                // await isPausedFar();
                // try {
                //                //     runTask.latestActiveTime = Date.now();
                //     await this.doGetSharedDataFar();
                // } catch (e) {
                //                //     let msg = ['获取登录信息失败!'];
                //     this.$message({message: msg, level: 'error'});
                //     console.log(...msg, e);
                //     this.exit = true;
                //     this.doStopFar();
                //     continue;
                // }
                // ========= 检查登录信息
                // await isPausedFar();
                // try {
                //                //     runTask.latestActiveTime = Date.now();
                //     await this.doCheckTaskFar();
                // } catch (e) {
                //                //     let msg = ['检查任务失败!', e];
                //     console.log(...msg);
                //     this.$message({message: msg, level: 'error'});
                //     this.exit = true;
                //     this.doStopFar();
                //     continue;
                // }
                // ==========  执行任务
                if (!runTask.running) {
                    this.doStopFar();
                    continue;
                }
                if (runTask.paused) {
                    continue;
                }
                // ====== 一个执行节点
                /*
                获取页面源码
                 */
                await isPausedFar();
                try {
                    runTask.latestActiveTime = Date.now();
                    await this.doGetHtmlFar();
                } catch (e) {
                    console.log('获取页面源码失败! url=' + this.currentUrl, e);
                    await this.doPunishFar(this.punishMs);
                    continue;
                }
                /*
                获取页面源码图片及更新
                 */
                while (this.current !== -1) {
                    await this.sleepFar(this.sleepTimeMs);
                    await isPausedFar();
                    if (!isRunningFar()) {
                        break;
                    }
                    try {
                        runTask.latestActiveTime = Date.now();
                        await this.doGetPicFar();
                    } catch (e) {
                        console.log('下载图片失败! uid=' + this.currentUrl, e);
                        await this.doPunishFar(this.punishMs);
                    }
                }
                /*
                上传未传完的页面源码数据
                 */
                // this.tempPostEdgesFailCount = 0;
                // this.updateEdgeListIndex = 0;
                // this.updateEdgeListSize = this.updateEdgeList.length;
                // while (this.updateEdgeList.length > 0) {
                //     await this.sleepFar(this.sleepTimeMs);
                //     await isPausedFar();
                //     if (!isRunningFar()) {
                //         break;
                //     }
                //     try {
                //         runTask.latestActiveTime = Date.now();
                //         await this.doUpdateEdgeFar();
                //     } catch (e) {
                //         console.log('上传页面源码失败! uid=' + this.currentUrl, e);
                //         await this.doPunishFar(this.punishMs);
                //     }
                // }

                if (!isRunningFar()) {
                    console.log('一次循环执行完毕', startCount++);
                    break;
                }
                this.updateFar();
            }
            this.doStopFar();
            console.log('任务退出!')
        },
        async doPunishFar(timeMs = 0) {
            this.timeoutMs = this.timeoutMs + this.punishMs;
            await this.sleepFar(this.timeoutMs);
            this.timeoutMs = 0;
        },
        updateFar() {
            let {runTask} = this.sharedState;
            if (runTask) {
                console.log('更新任务');
                this.$emit('update', runTask);
            }
        },
        doGetTotalFar() {
            return new Promise(async (resolve, reject) => {
                let param = {name: tbl.url}
                await api.mongo.count(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        let total = res.data.data;
                        resolve(total);
                        return;
                    }
                    console.error("读取链接总数失败!", res);
                    resolve(0);
                }).catch(e => {
                    console.error("读取链接总数失败!", e);
                    resolve(0);
                })
            })
        },
        /**
         * 获取未下载的链接数量
         * @returns {Promise<int>}
         */
        doGetLeftFar() {
            return new Promise(async (resolve, reject) => {
                let param = {name: tbl.url,status: null}
                await api.mongo.count(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        let total = res.data.data;
                        resolve(total);
                        return;
                    }
                    console.error("读取未下载链接总数失败!", res);
                    resolve(-1);
                }).catch(e => {
                    console.error("读取未下载链接总数失败!", e);
                    resolve(-1);
                })
            })
        },
        doResetFar() {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.url,
                    condition: {
                       status: {$ne: null}, isItem: 0
                    },
                    update: {
                        status: null
                    }
                }
                await api.mongo.updateMany(param).then(res => {
                    if (res.data && res.data.code === 0 && res.data.data) {
                        let update = res.data.data;
                        let {matchedCount, modifiedCount} = update;
                        let msg = [`重置所有链接成功`,`匹配${matchedCount}条数据`,`更新${modifiedCount}条数据`];
                        console.log(...msg);
                        this.$message({message: msg});
                        resolve(update);
                        return;
                    }
                    console.error("重置所有链接失败!", res);
                    resolve(null);
                }).catch(e => {
                    console.error("重置所有链接失败!", e);
                    resolve(null);
                })
            })
        },
        doGetCurrentFar() {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.url,
                    status: tbs.ok
                }
                await api.mongo.countCondition(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        let total = res.data.data;
                        resolve(total);
                        return;
                    }
                    console.error("读取链接下载总数失败!", res);
                    resolve(0);
                }).catch(e => {
                    console.error("读取链接下载总数失败!", e);
                    resolve(0);
                })
            })
        },
        doGetItemTotalFar() {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.url,
                    isItem: 1
                }
                await api.mongo.countCondition(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        let total = res.data.data;
                        resolve(total);
                        return;
                    }
                    console.error("读取链接商品总数失败!", res);
                    resolve(0);
                }).catch(e => {
                    console.error("读取链接商品总数失败!", e);
                    resolve(0);
                })
            })
        },
        doGetConfigFar() {
            return new Promise(((resolve, reject) => {
                api.config.getAll().then(res => {
                    if (res.data && res.data.code === 0) {
                        if (res.data.data.length > 0) {
                            let config = res.data.data[0];
                            resolve(config);
                            return;
                        }
                    }
                    resolve({});
                }).catch((e) => {
                    console.error("读取配置文件失败!", e);
                    resolve({});
                })
            }))
        },
        async sleepFar(sleepMs = this.sleepTimeMs) {
            return new Promise(((resolve, reject) => {
                setTimeout(() => {
                    resolve()
                }, sleepMs)
            }))
        },
        doStopFar() {
            let {runTask} = this.sharedState;
            if (runTask) {
                runTask.paused = false;
                runTask.running = false;
                this.updateFar();
                this.sharedState.runTask = null;
                this.exit = true;
                let {taskName = ''} = runTask;
                this.$message({message: ['任务结束', taskName]});
            }
        },
        /**
         * 获取网页内容
         * @returns {Promise<unknown>}
         */
        async doGetHtmlFar() {
            return new Promise(async (resolve, reject) => {
                let {runTask} = this.sharedState;
                if (!runTask) {
                    let msg = ['获取 任务信息 异常!', 'runTask:' + typeof (runTask)];
                    console.error(...msg);
                    this.$message({message: msg, level: 'error'});
                    reject(msg);
                    return;
                }
                try {
                    let url = await this.doGetUrlNot();
                    let urlTotalNot = await this.doGetUrlTotalNot();
                    console.log('更新未下载链接总数, urlTotalNot', urlTotalNot);
                    runTask.total = urlTotalNot;
                    let current = await this.doGetCurrentFar();
                    let itemTotal = await this.doGetItemTotalFar();
                    runTask.current = current;
                    runTask.itemTotal = itemTotal;
                    runTask.percentSuccess = current / urlTotalNot * 100;
                    this.currentUrl = url;
                    if (!url) {
                        let msg = ['获取待下载url异常!', 'url:' + typeof (url)];
                        console.error(...msg);
                        this.$message({message: msg, level: 'error'});
                        reject(msg);
                        return;
                    }
                    // 获取网页内容
                    let resHtml = await axios.get(url);
                    if (resHtml.data && resHtml.data.length > 0) {
                        let html = resHtml.data;
                        let finalUrl = this.fixUrl(resHtml.finalUrl);
                        let status = resHtml.status;
                        // toLearn: 这里搞不懂, 进入一个新的作用域, url变成了undefined
                        // 已修复, 因为下面定义了一个新的变量, 变量名为url, 导致url变成了undefined
                        // 2022年4月26日, 发现这里的url是没有值的, 只有finalUrl有值
                        if (url !== finalUrl) {
                            console.log("---doGetHtmlFar---获取的url不是最终的url,将更新url! url: %s -> %s", url, finalUrl);
                            await this.doUpdateUrlStatus(url, tbs.exclude);
                            url = finalUrl;
                        }
                        let pageOrItemObj = await this.doGetPageOrItemObj(url, html);
                        let {itemData, pageData} = pageOrItemObj;
                        // 2022年6月29日, 已下架或其他原因获取到的itemMainType为未知的不保存(防止覆盖已有数据)
                        if (itemData && itemData.itemMainType !== '未知') {
                            console.log('---doGetHtmlFar---itemData: %s', itemData);
                            let resItem = await this.doUpdateItem(itemData);
                            if (resItem.data && resItem.data.code === 0) {
                                await this.doUpdateImgList(itemData);
                                let msg = ['更新item成功!', 'itemId:' + itemData.itemId];
                                console.log(...msg);
                                this.$message({message: msg, level: 'success'});
                                this.itemData = itemData;
                                this.current = 0;
                                await this.doUpdateType(itemData);
                            } else {
                                let msg = ['更新item失败!', 'itemId:' + itemData.itemId];
                                console.error(...msg);
                                this.$message({message: msg, level: 'error'});
                                reject(msg);
                                return;
                            }
                        } else {
                            this.itemData = null;
                        }
                        if (pageData) {
                            pageData.statusCode = status;
                            console.log('---doGetHtmlFar---pageData:', pageData);
                            let resPage = await this.doUpdatePage(pageData);
                            if (resPage.data && resPage.data.code === 0) {
                                let msg = ['更新page成功!', 'id:' + pageData.id];
                                console.log(...msg);
                                this.$message({message: msg, level: 'success'});
                            } else {
                                let msg = ['更新page失败!', 'id:' + pageData.id];
                                console.error(...msg);
                                this.$message({message: msg, level: 'error'});
                                reject(msg);
                                return;
                            }
                        } else {
                            console.log('---doGetHtmlFar---pageData: %s', pageData);
                            reject(null);
                            return;
                        }
                        // toLearn 这里定义url必须取一个别名, 否则导致外面的url变成undefined
                        let {id, url: pageUrl, urls} = pageData;
                        await this.doUpdateUrls(pageUrl, urls);
                        await this.doUpdateUrlOk(id);
                        console.log('---doGetHtmlFar---获取url成功! url: %s', url);
                        console.log('清除邮箱计数');
                        localStorage.setItem("emailCount", "0");
                        resolve(pageData);
                    } else {
                        let msg = ['获取网页内容异常!', 'html:' + typeof (resHtml)];
                        console.error(...msg);
                        this.$message({message: msg, level: 'error'});
                        await this.doUpdateUrlError(url);
                        reject(msg);
                    }
                } catch (e) {
                    console.error("---doGetHtmlFar---获取网页内容失败!", e);
                    let msg = ['获取网页内容失败!', 'url:' + url];
                    this.$message({message: msg, level: 'error'});
                    await this.doUpdateUrlError(url);
                    reject(e);
                }
            })
        },
        /**
         * 获取页面或商品信息
         * @param url 商品链接
         * @param html 商品页面html
         * @returns {undefined}
         */
        async doGetPageOrItemObj(url, html) {
            console.log('-----doGetPageOrItemObj-----', url);
            return new Promise(async (resolve, reject) => {
                window.html = html;
                window.url = url;
                let dom = this.dp.parseFromString(html, 'text/html');
                let z = zp(dom);
                let title = this.fixTitle(z.find('title').text());
                let lang = this.getLanguage(url);
                let id = sha1(url);
                let urls = Array.from(new Set(z.find('a').get()
                    .map(e => e.href)
                    .filter(u => u) // 去除空值
                    .map(u => this.fixUrl(u)))) // 修正url
                let itemData, pageData;
                let isItem = url.includes('-item-') ? 1 : 0;
                if (isItem) {
                    let itemOriginText = html.between('<script>window.__HYDRATION_STATE__=', '</script>', true);
                    let itemOrigin = {};
                    try {
                        let jsonText = eval(itemOriginText);
                        itemOrigin = JSON.parse(jsonText);
                    } catch (e) {
                        console.error('---doGetPageOrItemObj---获取itemOrigin失败!', e);
                    }
                    let itemId = url.split('-item-')[1].split('.')[0];
                    let brandNameEl = z.find('a[data-tstid="cardInfo-title"]').get(0);
                    let brandName = brandNameEl ? brandNameEl.innerText.trim() : '未知';  //品牌名称
                    let itemNameEl = z.find('span[data-tstid="cardInfo-description"]').get(0);
                    let itemName = itemNameEl ? itemNameEl.innerText.trim() : '未知';  //商品名称
                    let priceEl = z.find('span[data-tstid="priceInfo-original"]').get(0);
                    let price = priceEl ? priceEl.innerText.trim() : '未知';  //商品价格
                    let sizeList = z.find('span[data-tstid="sizeDescription"]').get().map(e => e.innerText.trim()); //商品尺码
                    let deliveryTime = this.getDeliveryTime(z, itemOrigin);  //商品发货时间
                    let sameStyleList = this.getSameStyleList(z);  //同款
                    let imgList = this.getImgList(z, itemOrigin);  //图片
                    let breadNavList = this.getItemBreadNavList(z);  //面包屑导航
                    let itemMainType = breadNavList.length === 4 ? breadNavList[0] : '未知';  //商品主类型
                    let itemBrand = breadNavList.length === 4 ? breadNavList[1] : '未知';  //商品品牌
                    let itemType = breadNavList.length === 4 ? breadNavList[2] : '未知';  //商品类型
                    let itemSubType = breadNavList.length === 4 ? breadNavList[3] : '未知';  //商品子类型
                    let detail = this.getItemDetail(z);  //商品详情
                    let highLightList = this.getItemHighLightList(z);  //商品高亮
                    let composition = this.getItemComposition(z);  //商品成分
                    let brandStyleId = z.find('p[data-tstid="designerStyleId"] span').text();  //品牌风格id
                    itemOrigin.id = id;
                    itemOrigin.url = url;
                    itemOrigin.status = itemOrigin['apolloInitialState'] ? tbs.ok : tbs.fail;
                    itemOrigin.itemId = itemId;
                    itemOrigin.title = title;
                    itemOrigin.itemName = itemName;
                    itemOrigin.brandName = brandName;
                    itemData = {
                        id,
                        itemId,
                        brandName,
                        itemName,
                        price,
                        sizeList,
                        deliveryTime,
                        sameStyleList,
                        imgList,
                        breadNavList,
                        itemMainType,
                        itemBrand,
                        itemType,
                        itemSubType,
                        detail,
                        highLightList,
                        composition,
                        brandStyleId,
                        lang,
                        url,
                        title,
                        itemOrigin,
                    };
                }
                pageData = {
                    id,
                    lang,
                    url,
                    title,
                    html: [html],
                    isItem,
                    urls
                };
                let data = {
                    itemData,
                    pageData
                };
                resolve(data);
            })
        },
        getImgList(z, itemOrigin = {}) {
            let list = z.find('img[data-test^="imagery-img"]').get().map(e => e.src).map((src, ind) => {
                let url = this.fixUrl(src);
                let id = sha1(url);
                let index = ind + 1;
                return {
                    id, url, index,
                }
            });
            try {
                let productViewModel = this.getProductViewModel(itemOrigin);
                list = productViewModel.images.main.map((e, ind) => {
                    let url = this.fixUrl(e['zoom']);
                    let id = sha1(url);
                    let index = e['index'] || ind + 1;
                    return {
                        id, url, index,
                    }
                });
                return list;
            } catch (e) {
                console.log(e);
                return list;
            }
        },
        getItemComposition(z) {
            let composition = '未知';
            // 英文
            if (composition === '未知') {
                // 备注: 中文与英文的结构不同
                let compositionEl = z.find('div[data-tstid="theDetails-part2"] div').get().filter(e => zp(e).text().startsWith('Composition')).map(e => zp(e).find('p').get())[0];
                composition = compositionEl ? compositionEl.map(e => e.innerText).join(' ') : '未知';  //商品成分
            }
            // 中文
            if (composition === '未知') {
                // 备注: 中文与英文的结构不同
                let compositionEl = z.find('div[data-tstid="theDetails-part2"] div').get().filter(e => zp(e).text().startsWith('成分')).map(e => zp(e).find('p').get())[0];
                composition = compositionEl ? compositionEl.map(e => e.innerText).join(' ') : '未知';  //商品成分
            }
            return composition;
        },
        getItemHighLightList(z) {
            let highLightList = [];
            // 英文
            if (highLightList.length === 0) {
                // 备注: 中文与英文的结构不同
                let highLightEl = z.find('div').get().filter(el => zp(el).find('h4').get(0)).filter(el => zp(el).text().startsWith('Highlights'))[0];
                highLightList = highLightEl ? zp(highLightEl).find('li').get().map(e => e.innerText.trim()) : [];  //商品亮点
            }
            // 中文
            if (highLightList.length === 0) {
                // 备注: 中文与英文的结构不同
                let highLightEl = z.find('div').get().filter(el => zp(el).find('h4').get(0)).filter(el => zp(el).text().startsWith('设计亮点'))[0];
                highLightList = highLightEl ? zp(highLightEl).find('li').get().map(e => e.innerText.trim()) : [];  //商品亮点
            }
            return highLightList;
        },
        getItemDetail(z) {
            let detail = '未知';
            // 英文
            if (detail === '未知') {
                // 备注: 中文与英文的结构不同
                let detailEl = z.find('div[data-tstid="productDetails"] p[class*="-Body"]').get(0);
                detail = detailEl ? detailEl.innerText.trim() : '未知';  //商品详情
            }
            // 中文
            if (detail === '未知') {
                // 备注: 中文与英文的结构不同
                let detailEl = z.find('p[data-tstid="fullDescription"]').get(0);
                detail = detailEl ? detailEl.innerText.trim() : '未知';  //商品详情
            }
            return detail;
        },
        getItemBreadNavList(z) {
            return z.find('span[itemprop="name"]').get().map((el, ind) => {
                let breadName = el.innerText.trim();
                let url = this.fixUrl(el.parentElement.href);
                return {breadName, url, level: ind + 1}
            })
        },
        getSameStyleList(z) {
            return z.find('div[data-tstid="sameStyleSlider"] a').get().map(a => {
                let url = this.fixUrl(a.href);
                let itemId = sha1(url);
                let imgEl = zp(a).find('img').get(0)
                let imgUrl, imgId, itemName = ''
                if (imgEl) {
                    imgUrl = this.fixUrl(imgEl.src);
                    imgId = imgUrl ? sha1(imgUrl) : '';
                    itemName = zp(imgEl).attr('alt') || '';
                }
                return {
                    itemId, url, imgId, imgUrl, itemName
                }
            });
        },
        getDeliveryTime(z, itemOrigin = {}) {
            // 不准切 z.find('span[dir="ltr"]').last().text()
            let deliveryTime = '未知';
            // 英文
            if (deliveryTime === '未知') {
                // 备注: 中文与英文的结构不同
                let deliveryTimeEl = z.find('p').get().filter(p => p.innerText === 'Estimated delivery')[0];
                deliveryTime = deliveryTimeEl ? deliveryTimeEl.nextElementSibling.innerText.trim() : '未知';  //商品详情
            }
            // 中文
            if (deliveryTime === '未知') {
                // 备注: 中文与英文的结构不同
                let deliveryTimeEl = z.find('p').get().filter(p => p.innerText === '预计送达时间')[0];
                deliveryTime = deliveryTimeEl ? deliveryTimeEl.nextElementSibling.innerText.trim() : '未知';  //商品详情
            }
            if (deliveryTime === '未知') {
                try {
                    let productViewModel = this.getProductViewModel(itemOrigin);
                    let eddExpress = productViewModel['shippingInformations']['details']['default']['eddExpress'];
                    deliveryTime = eddExpress ? eddExpress.trim() : '未知';
                } catch (e) {
                    console.log(e);
                }
            }
            return deliveryTime;
        },
        getProductViewModel(itemOrigin) {
            try {
                let rootQuery = itemOrigin['apolloInitialState']['ROOT_QUERY'] || {};
                let initKey = Object.keys(rootQuery).filter(k => typeof (rootQuery[k]) !== 'string')[0] || Object.keys(rootQuery).filter(k => k !== '__typename')[0];
                let data = rootQuery[initKey].data || {};
                let productViewModel = data['slice-product']['productViewModel'] || {};
                if (productViewModel) {
                    return productViewModel;
                }
            } catch (e) {
                console.log(e);
            }
            return {};
        },
        getLanguage(url) {
            let lang = 'en';
            if (url.indexOf('/cn/') > -1) {
                lang = 'cn';
            }
            return lang;
        },
        fixUrl(url = '') {
            url = url.split('#')[0]; // 去掉hash
            // 保留参数 url = url.split('?')[0]; // 去掉参数
            // 修正域名
            url = url.startsWith('https://www.farfetch.cn/') ? url : url.replace(window.location.href.split('#')[0], 'https://www.farfetch.cn/');
            return url;
        },
        fixTitle(title) {
            let arr = title.split('-');
            if (arr.length > 1) {
                arr.pop();
            }
            return arr.join('-').trim();
        },
        /**
         * 下载页面图片数据
         * @returns {Promise<unknown>}
         */
        doGetPicFar() {
            return new Promise(async (resolve, reject) => {
                if (!this.itemData) {
                    this.current = -1
                    resolve([]);
                    return;
                }
                let {id: itemBaseId, itemId, url: itemUrl, imgList = [], title} = this.itemData;
                for (let i = 0; i < imgList.length; i++) {
                    const img = imgList[i];
                    let {id: imgId, url, index} = img;
                    if (url) {
                        await axios.get(url, {responseType: 'blob'}).then(async res => {
                            if (res.status === 200) {
                                let blob = res.data;
                                // 文件名规范: data-[itemId]-[imgId].jpg; 必须 data- 开头, 中间用 - 分隔, 只有两个 - 分隔;
                                let fileName = `data-${itemId}-${index}_${imgId}.jpg`;
                                let fileDiskName = `${itemId}/${index}_${imgId}.jpg`;
                                let size = blob.size;
                                let base64 = await blob64.serializePromise(blob);
                                /*id,url,index,itemBaseId,itemId,itemUrl,title*/
                                let imgData = {
                                    id: imgId,
                                    url,
                                    index,
                                    itemBaseId,
                                    itemId,
                                    itemUrl,
                                    title,
                                    fileName,
                                    fileDiskName,
                                    size,
                                    // base64, // 原始base64, 不需要
                                    status: tbs.ok,
                                };
                                /*id,url,index,itemBaseId,itemId,itemUrl,title*/
                                await this.doUpdateImgOk(imgData);
                                await this.doUploadImg(blob, fileName);
                                let msg = ['下载图片成功', `${i + 1} of ${imgList.length}`, '图片大小: ' + size];
                                console.log('---doGetPicFar---', ...msg);
                                this.$message({message: msg, level: 'success'});
                                await this.sleepFar(this.sleepTimeMs);
                            } else {
                                let msg = ['下载图片失败', `状态码错误[${res.status}]`, `${url} 请求失败`];
                                console.log(...msg);
                                this.$message({message: msg, level: 'error'});
                                await this.doUpdateImgErr(imgId);
                                reject(msg);
                            }
                        }).catch(async err => {
                            console.log(err);
                            let msg = ['下载图片失败', `${i} of ${imgList.length}`, err.message];
                            console.log(...msg);
                            this.$message({message: msg, level: 'error'});
                            await this.doUpdateImgErr(imgId);
                            reject(err);
                        })
                    }
                }
                this.current = -1;
                resolve('---doGetPicFar---success!');
            })
        },
        async doUpdateItem(itemData) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.item,
                }
                Object.assign(param, itemData);
                delete param.itemOrigin;
                let itemOrigin = itemData.itemOrigin;
                await this.doUpdateItemOrigin(itemOrigin);
                let resUpdateItem = await api.mongo.save(param);
                resolve(resUpdateItem);
            })
        },
        doUpdatePage(pageData) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.page,
                }
                Object.assign(param, pageData);
                let resUpdatePage = await api.mongo.save(param);
                resolve(resUpdatePage);
            })
        },
        doUpdateUrls(url, urls) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.url,
                }
                let data = urls.map(u => {
                    let id = sha1(u);
                    let isItem = u.includes('-item-') ? 1 : 0;
                    let urlData = {id, url: u, from: url, isItem};
                    // 排除无效的url, 设置无效的url状态为 3 - 排除
                    if (!u.startsWith('https://www.farfetch.cn/cn/')) {
                        urlData.status = tbs.exclude;
                    }
                    return urlData;
                });
                await api.mongo.saveMany([param, data]).then(res => {
                    if (res.data && res.data.code === 0) {
                        console.log("---urls---更新成功", res, "更新数量", data.length);
                        resolve(res.data.data);
                    } else {
                        console.error("---urls---更新失败", res);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---urls---更新失败", err);
                    reject(err);
                })
            });
        },
        doUpdateUrlOk(id) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.url,
                    id,
                    status: tbs.ok,
                };
                await api.mongo.save(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        console.log("---doUpdateUrlOk---更新成功", res, id);
                        resolve(res.data.data);
                    } else {
                        console.error("---doUpdateUrlOk---更新失败", res, id);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---doUpdateUrlOk---更新失败", err, id);
                    reject(err);
                })
            });
        },
        doUpdateUrlStatus(url, status) {
            return new Promise(async (resolve, reject) => {
                let id = sha1(url);
                // 获取tbs对象的所有属性值
                let allStatus = Object.values(tbs);
                if (!allStatus.includes(status)) {
                    console.error("---doUpdateUrlStatus---status不存在", status);
                    reject(`status: ${status} 不在枚举中`);
                    return;
                }
                let param = {
                    name: tbl.url,
                    id,
                    status: status,
                };
                await api.mongo.save(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        console.log("---doUpdateUrlStatus---更新成功", res, id, status);
                        resolve(res.data.data);
                    } else {
                        console.error("---doUpdateUrlStatus---更新失败", res, id, status);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---doUpdateUrlStatus---更新失败", err, id, status);
                    reject(err);
                })
            });
        },
        doUpdateUrlError(url) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.url,
                    id: sha1(url),
                    url,
                    status: tbs.fail,
                };
                await api.mongo.save(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        console.log("---doUpdateUrlError---更新成功", res, url);
                        resolve(res.data.data);
                    } else {
                        console.error("---doUpdateUrlError---更新失败", res, url);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---doUpdateUrlError---更新失败", err, url);
                    reject(err);
                })
            })
        },
        doGetUrlNot() {
            return new Promise(async (resolve, reject) => {
                // 获取一个衣服未下载链接
                let paramClothUrl = {
                    name: tbl.url,
                    start: 1,
                    size: 1,
                    status: tbs.not,
                    from: {[tbc.like]: '/clothing-'}
                };
                // 获取一个 衣服列表未下载链接
                let paramClothListUrl = {
                    name: tbl.url,
                    start: 1,
                    size: 1,
                    status: tbs.not,
                    url: {[tbc.like]: '/clothing-'}
                };
                // 获取任意一个未下载链接
                let paramAnyUrl = {
                    name: tbl.url,
                    start: 1,
                    size: 1,
                    status: tbs.not,
                    url: {[tbc.notIn]: [null, '']}
                };
                let resPageNot;
                // 优先获取一个衣服未下载链接,如果没有,则获取一个衣服列表未下载链接,如果没有,则获取任意一个未下载链接
                resPageNot = await api.mongo.find(paramClothUrl);
                if (!(resPageNot.data && resPageNot.data.code === 0 && resPageNot.data.data.length > 0)) {
                    console.log("---doGetUrlNot---没有衣服未下载链接");
                    resPageNot = await api.mongo.find(paramClothListUrl);
                } else {
                    console.log("---doGetUrlNot---有衣服未下载链接", resPageNot.data.data[0].url);
                }
                if (!(resPageNot.data && resPageNot.data.code === 0 && resPageNot.data.data.length > 0)) {
                    console.log("---doGetUrlNot---没有衣服列表未下载链接");
                    resPageNot = await api.mongo.find(paramAnyUrl);
                }
                let data = {};
                if (resPageNot.data && resPageNot.data.code === 0) {
                    let list = resPageNot.data.data;
                    if (list && list.length > 0) {
                        data = list[0];
                    } else {
                        data.url = 'https://www.farfetch.cn/cn/';
                        console.log("---doGetHtmlFar---没有链接可以获取! 将获取首页链接!");
                    }
                } else {
                    console.error("---doGetUrlNot---获取失败", resPageNot);
                    resolve('');
                    return;
                }
                let {url = '', id = ''} = data;
                url = this.fixUrl(url);
                if (url) {
                    resolve(url);
                } else {
                    resolve('');
                }
            })
        },
        doGetUrlTotalNot() {
            return new Promise(async (resolve, reject) => {
                // 获取一个链接
                let param = {
                    name: tbl.url,
                    status: tbs.not,
                    url: {[tbc.notIn]: [null, '']}
                };
                let resCount = await api.mongo.countCondition(param);
                if (resCount.data && resCount.data.code === 0) {
                    let count = resCount.data.data;
                    resolve(count);
                } else {
                    console.error("---doGetUrlNot---获取失败", resCount);
                    reject(resCount);
                }
            })
        },
        doUpdateImgList(itemData) {
            return new Promise(async (resolve, reject) => {
                let {imgList, id: itemBaseId, itemId, url: itemUrl, title} = itemData;
                let param = {
                    name: tbl.img,
                };
                let data = imgList.map(img => {
                    let {id, url, index} = img;
                    return {
                        id,
                        url,
                        index,
                        itemBaseId,
                        itemId,
                        itemUrl,
                        title
                    }
                });
                await api.mongo.saveMany([param, data]).then(res => {
                    if (res.data && res.data.code === 0) {
                        resolve(res);
                    } else {
                        console.error("---doUpdateImgNot---保存失败", res);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---doUpdateImgNot---保存失败", err);
                    reject(err);
                })
            })
        },
        doUpdateItemOrigin(itemOrigin) {
            return new Promise(async (resolve, reject) => {
                let {id = '', url = ''} = itemOrigin;
                if (!id || !url) {
                    resolve('');
                    return;
                }
                let param = {
                    name: tbl.itemOrigin,
                };
                Object.assign(param, itemOrigin);
                await api.mongo.save(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        console.log("---doUpdateItemOrigin---更新成功", res, itemOrigin);
                        resolve(res.data.data);
                    } else {
                        console.error("---doUpdateItemOrigin---更新失败", res, itemOrigin);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---doUpdateItemOrigin---更新失败", err, itemOrigin);
                    reject(err);
                })
            })
        },
        doUpdateImgOk(imgData) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.img,
                }
                Object.assign(param, imgData);
                let resUpdateImg = await api.mongo.save(param);
                resolve(resUpdateImg);
            })
        },
        doUpdateImgErr(imgId) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.img,
                    id: imgId
                }
                let resUpdateImgErr = await api.mongo.save(param);
                resolve(resUpdateImgErr);
            })
        },
        doUploadImg(blob, fileName) {
            return new Promise((resolve, reject) => {
                let fd = new FormData();
                fd.append('file', blob, fileName);
                let gmParam = {
                    url: appConfig.host + 'file',
                    method: 'post',
                    data: fd,
                    overrideMimeType: 'multipart/form-data',
                    onload: (resUp) => {
                        if (resUp.status === 200 && resUp.responseText.includes('upFile : true')) {
                            console.log('---doUploadImg---上传图片成功!', '');
                        } else {
                            console.log('---doUploadImg---上传图片失败!');
                            reject(resUp);
                            return;
                        }
                        resolve(resUp);
                    },
                    onerror: (eUp) => {
                        console.log('---doUploadImg---上传图片时发生错误!', eUp);
                        reject(eUp);
                    }
                };
                GM_xmlhttpRequest(gmParam);
            });
        },
        doUpdateType(itemData) {
            return new Promise(async (resolve, reject) => {
                let {itemMainType = '未知', itemType = '未知', itemSubType = '未知'} = itemData;
                if (!itemMainType || !itemType || !itemSubType) {
                    resolve('');
                    return;
                }
                if (itemMainType === '未知' || itemType === '未知' || itemSubType === '未知') {
                    resolve('');
                    return;
                }
                if (!itemMainType.breadName || !itemType.breadName || !itemSubType.breadName) {
                    resolve('');
                    return;
                }
                let param = {
                    name: tbl.itemType,
                };
                let typeLevel = itemMainType.breadName + '>>' + itemType.breadName + '>>' + itemSubType.breadName;
                let id = sha1(typeLevel);
                let typeData = {
                    id,
                    itemMainType,
                    itemType,
                    itemSubType,
                    typeLevel,
                };
                Object.assign(param, typeData);
                await api.mongo.save(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        console.log("---doUpdateType---更新成功", res, itemData);
                        resolve(res.data.data);
                    } else {
                        console.error("---doUpdateType---更新失败", res, itemData);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---doUpdateType---更新失败", err, itemData);
                    reject(err);
                })
            })
        },
    },
    mounted() {
        console.log('document.cookie', document.cookie);
        console.log(this.$options.name + ' mounted', this.runnerId);
        return this.doInitFar();
    },
    unmounted() {
        console.log(this.$options.name + ' unmounted', this.runnerId);
        this.exit = true;
        this.updateFar();
    },
};
var template = `
<div class="heartbeat">
</div>
`;

let HeartBeat = {
    name: 'HeartBeat',
    components: {},
    template,
    props: {
        list: {
            type: Array,
            default: []
        },
        heartBeat: {
            type: Boolean,
            default: false
        },
    },
    methods: {
        doHeartBeat() {
            return new Promise((resolve, reject) => {
                let result = false;
                api.middle.heartBeat().then(res => {
                    if (res.data && res.data.code === 0) {
                        result = true;
                    } else {
                        console.warn('心跳异常! %s', new Date().fmt(), res)
                    }
                    resolve(result);
                }).catch((e) => {
                    window.e = e;
                    console.error('心跳失败! %s %s', new Date().fmt(), e);
                    resolve(result);
                })
            });
        },
        doNextTimeOut() {
            this.lastTimeOutId = setTimeout(() => {
                clearTimeout(this.lastTimeOutId);
                if (this.exit) {
                    return;
                }
                this.doHeartBeat().then(success => {
                    if (success) {
                        this.timeoutMs = appConfig.heartBeatFixedDelay * 1000;
                        this.lastCostTime = Date.now() - this.lastSuccessTime;
                        this.lastSuccessTime = Date.now();
                        console.log('[%s] 心跳正常! 间隔%s秒', new Date().fmt(), this.lastCostTime / 1000);
                    } else {
                        this.timeoutMs = this.timeoutMs + this.punishMs;
                        console.warn('心跳超时惩罚: %ss', this.timeoutMs / 1000);
                    }
                    this.doNextTimeOut();
                });
            }, this.timeoutMs)
        }
    },
    mounted() {
        console.log('HeartBeat mounted');
        this.doNextTimeOut();
    },
    unmounted() {
        console.log('HeartBeat unMounted, lastTimeOutId: %s', this.lastTimeOutId);
        clearTimeout(this.lastTimeOutId);
        this.exit = true;
        console.log('HeartBeat exit!')
    },
    data() {
        const {useSlots} = Vue;
        return {
            punishMs: appConfig.heartBeatPunish * 1000,
            lastTimeOutId: -1,
            timeoutMs: appConfig.heartBeatFixedDelay * 1000,
            lastSuccessTime: Date.now(),
            lastCostTime: -1,
            exit: false,
        }
    }
};// message.js
var template = `
    <div class="message" v-if="isShow">
        <div :class="'content ' + level">
            <button @click="close" :class="level">X</button>
            <div class="text" v-if="message instanceof Array" v-for="msg in message">
                {{ msg }}
            </div>
            <div class="text" v-else>
                {{ message }}
            </div>
        </div>
    </div>
`;

let Message = {
    name: 'Message',
    data() {
        return {
            list: []
        }
    },
    props: {
        message: {
            // @see <a href="https://v3.cn.vuejs.org/guide/component-props.html#prop-%E9%AA%8C%E8%AF%81">Vue3 Prop 验证</a>
            type: [String, Array], required: true,
        },
        level: {
            type: String, default: 'success'
        },
        duration: {
            type: Number, default: 3,
        },
    },
    setup(props) {
        let {ref} = Vue;
        let isShow = ref(false);
        const show = () => {
            isShow.value = true;
        };
        const close = () => {
            isShow.value = false;
        };
        let {level} = props;
        if ('success' === level) {
            store.state.lastMessageTime = Date.now();
        }
        return {isShow, show, close};
    },
    created() {
        this.show();
    },
    template,
    watch: {
        isShow(nVal, oVal) {

        }
    }
};
var template = `
<div class="crack">
    <div v-if="crackMsg.length > 0" class="msg">
        <div>-- 破解模块 --</div>
        <div v-for="key,index in crackMsg">[{{index+1}}] {{key}}</div>
        <div>破解耗时{{pastTime}}ms</div>
    </div>
    <div :class="'crack-'+!!queryHash" v-else>[破解结果] {{queryHash?'解密成功':'解密失败'}}</div>
    <hr/>
</div>
`;

let Crack = {
    name: 'Crack',
    components: {},
    template,
    data() {
        const {useSlots} = Vue;
        return {
            sharedState: store.state,
            controlSlot: !!useSlots().default,
            crackMsg: [],
            queryHash: '',
            parser: new DOMParser(),
            startTime: new Date().getTime(),
            pastTime: 0,
        }
    },
    methods: {
        async doCrack() {
            /*
            // https://www.instagram.com/
            dom=0;fetch('/').then(res=>res.text().then(t=>{p=new DOMParser();dom=p.parseFromString(t,'text/html')}))
            s=dom.documentElement.getElementsByTagName('script');
            src=Array.from(s).filter(el=>el.src).map(el=>el.src).filter(sr=>sr.indexOf('ConsumerLibCommons')>0)[0]
            js=0;fetch(src).then(res=>res.text().then(t=>js=t))
            queryHash=js.split('\n').filter(t=>t.indexOf('queryId')>0).join(';').split(';').filter(t=>t.indexOf('queryId')>0&&t.indexOf('timeout')>0)[0].split('queryId')[1].split('timeout')[0].split('"')[1].split('"')[0]
             */
            this.logMsg('开始破解[queryHash]')
            let src = '';
            await this.getForHomeHtml().then(html => {
                if (html) {
                    this.logMsg('已获取首页数据')
                } else {
                    this.logMsg('获取首页数据失败')
                }
                let dom = this.parser.parseFromString(html, 'text/html')
                let scriptElements = dom.documentElement.getElementsByTagName('script');
                src = Array.from(scriptElements).filter(el => el.src).map(el => el.src).filter(sr => sr.indexOf('ConsumerLibCommons') > 0)[0];
                if (src) {
                    this.logMsg('已获取密码地址')
                } else {
                    this.logMsg('获取密码地址失败')
                }
            }).catch(err => {
                this.logMsg('破解异常 err=' + String(err))
                console.log(err)
            })
            if (src) {
                await this.getForText(src).then(text => {
                    if (text) {
                        this.logMsg('已获取密码数据')
                    } else {
                        this.logMsg('获取密码数据失败')
                    }
                    this.logMsg('已获取密码数据')
                    let lines = text.split('\n').filter(t => t.indexOf('queryId') > 0).join(';').split(';').filter(t => t.indexOf('queryId') > 0 && t.indexOf('timeout') > 0)[0].split('queryId')[1].split('timeout').filter(t => t.indexOf('queryParams') > 0).join('').split('"')
                    if (lines.length === 3) {
                        this.queryHash = lines[1]
                        this.sharedState.queryHash = this.queryHash
                        this.logMsg('已解密密码数据')
                        this.logMsg('-- 破解成功 --')
                    } else {
                        this.logMsg('解密密码数据失败')
                    }
                }).catch(err => {
                    this.logMsg('获取密码数据失败 err=' + String(err))
                })
            }
            setTimeout(() => this.crackMsg = [], 10000)
        },
        logMsg(msg) {
            let newMsg = []
            if (Array.isArray(msg)) {
                msg.forEach(e => newMsg.push([new Date().fmt('[yyyy-MM-dd HH:mm:ss]'), e].join(' ')))
            } else {
                newMsg.push([new Date().fmt('[yyyy-MM-dd HH:mm:ss]'), msg].join(' '))
            }
            this.crackMsg.push(...newMsg)
            console.log(...newMsg)
            this.pastTime = new Date().getTime() - this.startTime
        },
        getForText(src) {
            return new Promise((resolve, reject) => {
                if (!src) {
                    let msg = ['src 地址不能为空 ', src]
                    this.$message({message: msg, level: 'error'})
                    console.log(...msg)
                    reject(msg)
                    return
                }
                axios.get(src).then(res => {
                    if (res.data && res.status === 200) {
                        let html = res.data
                        resolve(html)
                    } else {
                        let msg = ['获取TEXT失败', src]
                        this.$message({message: msg, level: 'error'})
                        console.log(...msg, res)
                        resolve('')
                    }
                }).catch(err => {
                    let msg = ['获取TEXT失败 err', src]
                    this.$message({message: msg, level: 'error'})
                    console.log(...msg, err)
                    resolve('')
                })
            })
        },
        getForHomeHtml() {
            return new Promise((resolve, reject) => {
                axios.get('/').then(res => {
                    if (res.data && res.status === 200) {
                        let html = res.data
                        resolve(html)
                    } else {
                        let msg = ['获取首页HTML失败']
                        this.$message({message: msg, level: 'error'})
                        console.log(...msg, res)
                        resolve('')
                    }
                }).catch(err => {
                    let msg = ['获取首页HTML失败 err']
                    this.$message({message: msg, level: 'error'})
                    console.log(...msg, err)
                    resolve('')
                })
            })
        }
    },
    mounted: () => {
        const {proxy} = Vue.getCurrentInstance();
        console.log('%s mounted', proxy.$options.name);
        proxy.doCrack();
    },
};
var template = `
<div class="task">
    <h3>任务管理</h3>
    <button @click="doAddFar" v-if="!isEdit">新增</button>
    <button @click="doCancelFar" v-if="isEdit">取消</button>
    <button @click="doRefreshFar">刷新</button>
    <button @click="doTestFar">测试</button>
    
    <Form :data="editModel" :showModel="formShowModel" :schema="schema" v-if="isEdit && !isTest" @cancel="doCancelFar" @submit="doSubmitFar"></Form>
    <Form :data="editModel" :showModel="formTestModel" :schema="schema" v-if="isEdit && isTest" @cancel="doCancelFar" @submit="doSubmitFarTest"></Form>
    <List :list="list" :showModel="listShowModel" :schema="schema">
        <template v-slot="{item}">
            <button @click="doStartFar(item)" v-if="!item.running">开始</button>
            <button @click="doPauseFar(item)" v-if="item.running&&!item.paused">暂停</button>
            <button @click="doResumeFar(item)" v-if="item.running&&item.paused">恢复</button>
            <button @click="doStopFar(item)" v-if="item.running">停止</button>
            <button @click="doEditFar(item)" :disabled="item.running">修改</button>
            <button @click="doDeleteFar(item)" :disabled="item.running">删除</button>
        </template>
    </List>
    <div v-if="banMsg && banMsg.length > 0">
        <div v-for="key in banMsg">
            {{banMsg[key]}}
        </div>
    </div>
<!--    <Crack></Crack>-->
    <div v-if="isHealthCheck" class="health">
        <div class="message" :class="'health-'+healthState">
            <span>健康监控</span>
            <span>{{healthState?'正常':'异常'}}</span>
            <span>&nbsp;{{timeUnit(pastSecond)}}&nbsp;</span>
            <span>阀值</span>
            <span>&nbsp;{{timeoutStr}}&nbsp;</span>
            <button @click="()=>healthCheckPausedFar=!healthCheckPausedFar">
            {{healthCheckPausedFar?'继续':'暂停'}}
            </button>
            <div v-for="key,index in healthMsg">[{{index+1}}] {{key}}</div>
        </div>
    </div>
    <RunnerFar v-if="sharedState.runTask" @update="doUpdateFar" @banMsg="doBanMsgFar"></RunnerFar>
</div>
`;


let Task = {
    name: 'Task',
    data() {
        return {
            list: [],
            isEdit: false,
            isTest: false,
            sharedState: store.state,
            // swagger中的schema
            schema: '任务信息',
            model: {
                taskName: '任务-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                threadSize: 1,
                first: '',
                total: '',
                itemTotal: '',
                current: '',
                percentSuccess: '',
                timeoutExecTime: '',
                running: false,
                paused: false,
                firstExecTime: '',
                lastExecTime: '',
            },
            modelTest: {
                url: 'https://www.instagram.com/graphql/query/?query_id=17888483320059182&id=4143607182&first=40',
                proxy: 'http://127.0.0.1:1080',
                headers: {
                    'Cookie': 'ds_user_id=51132914782; csrftoken=dNCZEujTqwDLbBPdi6hxExVBy2ymggfE; sessionid=51132914782%3AwJ2vpixHZOofYl%3A8',
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36',
                },
            },
            listShowModel: {
                taskName: '',
                // threadSize: '',
                // first: '',
                total: '',
                itemTotal: '',
                current: '',
                // percentSuccess: '',
                running: false,
                paused: false,
                updatedTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
            },
            formShowModel: {
                taskName: '',
                threadSize: 1,
                // first: '',
                // total: '',
                // itemTotal: '',
                // current: '',
                // percentSuccess: '',
                // running: false,
                // updatedTime: '',
            },
            formTestModel: {
                url: '',
                proxy: '',
                headers: {
                    type: 'map',
                },
            },
            editModel: {},
            heartBeat: false,
            banMsg: '',
            isUnMounted: false,
            isHealthCheck: appConfig.isHealthCheck,
            healthCheckPausedFar: false,
            pastSecond: 0,
            healthState: true,
            healthMsg: [],
            timeoutStr: -1,
        }
    },
    components: {List, Form, RunnerFar, Crack},
    template,
    mounted: () => {
        const {proxy} = Vue.getCurrentInstance();
        console.log('%s mounted', proxy.$options.name);
        proxy.doGetAllFar();
        if (proxy.isHealthCheck) {
            return proxy.doHealthCheckFar();
        }
    },
    unmounted() {
        this.isUnMounted = true;
    },
    methods: {
        timeUnit(second) {
            return timeUnit(second);
        },
        async doHealthCheckFar() {
            let startId = localStorage.getItem("startId")
            this.timeoutStr = this.timeUnit(Math.floor(appConfig.healthCheckIntervalMs / 1000))
            let autoStart = localStorage.getItem("autoStart")
            console.log('healthCheck start startId', startId)
            let isExecAutoStart = false
            while (!this.isUnMounted) {
                if (autoStart && !isExecAutoStart) {
                    let queryHash = 'this.sharedState.queryHash'
                    if (queryHash) {
                        isExecAutoStart = true
                        await this.isHealthCheckPausedFar()
                        setTimeout(this.doAutoStartFar, 5000)
                    } else {
                        let msg = ['自动启动任务无法执行', 'queryHash未破解']
                        console.log(...msg);
                        this.$message({message: msg, level: 'error'});
                    }
                }
                await this.isHealthCheckPausedFar()
                let {lastMessageTime = 0} = this.sharedState;
                let now = Date.now();
                let past = now - lastMessageTime;
                if (past > appConfig.healthCheckIntervalMs) {
                    this.healthState = false;
                    this.sharedState.healthState = this.healthState;
                    this.pastSecond = Math.floor(past / 1000);
                    let msg = [`健康检查超期 将在${this.pastSecond}秒后尝试重启任务`, this.timeUnit(this.pastSecond)];
                    this.healthMsg.push(msg[0])
                    console.log(...msg);
                    this.$message({message: msg, level: 'error'});
                    localStorage.setItem("autoStart", "true")
                    setTimeout(() => {
                        console.log('刷新页面')
                        let nextTimeout = 0;
                        // 停止任务
                        let {runTask} = this.sharedState;
                        if (runTask) {
                            nextTimeout += 1000;
                            this.doStopFar(runTask)
                        }
                        // 发送邮件
                        nextTimeout += 5000;
                        let emailCount = localStorage.getItem("emailCount") || 0;
                        if (!(emailCount && emailCount >= appConfig.emailCountIgnore)) {
                            emailCount++
                            localStorage.setItem("emailCount", emailCount);
                        } else {
                            this.doSendMailFar().then(() => {
                                let msg = `${this.timeUnit(nextTimeout / 1000)} 后刷新页面`
                                this.healthMsg.push(msg)
                                console.log(msg)
                                console.log('邮件发送成功, 清除邮箱计数');
                                localStorage.setItem("emailCount", "0");
                                // 刷新页面
                                setTimeout(() => history.go(0), nextTimeout)
                            })
                        }
                    }, 10000)
                    await this.sleepFar(20000);
                } else {
                    this.healthState = true;
                    this.sharedState.healthState = this.healthState;
                }
                this.pastSecond = Math.floor(past / 1000);
                await this.sleepFar(1000);
            }
            console.log('healthCheck stopped')
        },
        async sleepFar(sleepMs = this.sleepTimeMs) {
            return new Promise(((resolve, reject) => {
                setTimeout(() => {
                    resolve()
                }, sleepMs)
            }))
        },
        async isHealthCheckPausedFar() {
            while (this.healthCheckPausedFar) {
                await this.sleepFar(300)
            }
        },
        async doSendMailFar() {
            let emailList = [];
            await this.$api.notifyEmail.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    emailList = res.data.data
                    emailList = emailList
                        .filter(mail => mail.enabled && mail.email)
                        .map(mail => mail.email)
                    let msg = `获取邮件列表成功! 待通知邮箱(${emailList.length}个) ${emailList}`
                    this.healthMsg.push(msg)
                    console.log(msg, res)
                } else {
                    let msg = `获取邮件列表失败!`
                    this.healthMsg.push(msg)
                    console.log(msg, res, emailList)
                }
            }).catch(err => {
                let msg = `获取邮件列表异常!`
                this.healthMsg.push(msg)
                console.log('notifyEmail err', err)
            })
            let mailDto = {
                content: `${new Date().fmt('[far-爬虫告警][yyyy-MM-dd HH:mm:ss] ')}爬虫任务健康检查失败, 将在5秒后尝试重启任务; 请及时进行检查!\n[检查周期] ${this.timeoutStr}`,
                title: `《${new Date().fmt('[far-爬虫告警][yyyy-MM-dd HH:mm:ss] ')}任务健康检查告警》`,
                to: ""
            };
            this.healthMsg.push(`即将发送告警邮件`)
            this.healthMsg.push(`[标题] ${mailDto.title}`)
            this.healthMsg.push(`[内容] ${mailDto.content}`)
            for (let i = 0; i < emailList.length; i++) {
                const email = emailList[i];
                let msg = `尝试给${email}发送邮件`
                this.healthMsg.push(msg)
                let emailObj = {
                    content: mailDto.content,
                    title: mailDto.title,
                    to: email,
                }
                await this.$api.eMail.send(emailObj).then(res => {
                    if (res.data && res.data.code === 0) {
                        let msg = `给${email}发送邮件成功!`
                        this.healthMsg.push(msg)
                        console.log(msg, res)
                    } else {
                        let {data = {msg: ''}} = res
                        let msg = `给${email}发送邮件失败!${data.msg}`
                        this.healthMsg.push(msg)
                        console.log(msg, res)
                    }
                }).catch(err => {
                    let msg = `给${email}发送邮件异常!`
                    this.healthMsg.push(msg)
                    console.log('发送邮件异常!', err)
                })
            }
        },
        doAutoStartFar() {
            let msg = '触发自动启动任务'
            this.healthMsg.push(msg)
            console.log(msg)
            let startId = localStorage.getItem("startId")
            let find = this.list.filter(e => e.id === startId)
            let item = null;
            if (find && find.length > 0) {
                item = find[0];
            } else {
                if (this.list && this.list.length > 0) {
                    item = this.list[0]
                }
            }
            if (item) {
                let {taskName} = item
                let msg = ['健康检查 启动任务', taskName]
                this.healthMsg.push(...msg)
                console.log('健康检查 启动任务', item)
                this.doStartFar(item)
            } else {
                let msg = ['健康检查', '无可用启动任务']
                this.healthMsg.push(...msg)
                console.log(...msg, this.list)
                this.$message({message: msg, level: 'error'})
            }
        },
        doRefreshFar() {
            this.$api.taskFar.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                    let msg = '刷新成功';
                    this.$message({message: msg});
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '口调用接失败';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doGetAllFar() {
            this.$api.taskFar.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                    let {runTask, healthState = true} = this.sharedState;
                    if (!runTask) {
                        let list = deepClone(this.list)
                        list.sort(item => item.running ? -1 : 1).forEach(item => {
                            if (!healthState) {
                                healthState = true;
                                this.doStartFar(item);
                            }
                        })
                    }
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doAddFar() {
            this.editModel = deepClone(this.model);
            this.isEdit = true;
        },
        doEditFar(item) {
            this.editModel = deepClone(item);
            this.isEdit = true;
        },
        doDeleteFar(item) {
            this.$api.taskFar.delete(item).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '删除成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAllFar()
                } else {
                    let msg = '删除失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '删除失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doSubmitFar() {
            this.doUpdateFar(this.editModel);
        },
        doUpdateFar(item) {
            if (!item) {
                let msg = '保存失败!';
                console.error(msg, item);
                this.$message({message: msg, level: 'error'});
                return;
            }
            this.$api.taskFar.update(item).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '保存成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAllFar()
                } else {
                    let msg = '保存失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '保存失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doBanMsgFar(msg = null) {
            this.banMsg = msg;
        },
        doCancelFar() {
            this.isEdit = false;
            this.isTest = false;
        },
        doStartFar(item) {
            console.log('------------------start------------------');
            let {runTask} = this.sharedState;
            if (runTask) {
                let msg = "已有任务在运行 " + runTask.taskName;
                console.warn('[%s] '.replace('%s', new Date().fmt()) + msg);
                this.$message({message: ['已有任务在运行', runTask.taskName], level: 'error'});
            } else {
                this.sharedState.runTask = item;
                let {id} = item;
                localStorage.setItem("startId", id)
                item.running = true;
                let msg = [`任务启动`, item.taskName];
                console.log(...msg);
                this.$message({message: msg});
            }
        },
        doStopFar(item) {
            console.log('------------------stop------------------');
            item.running = false;
            item.paused = false;
            let {runTask} = this.sharedState;
            if (runTask && runTask.running && runTask.id === item.id) {
                runTask.running = false;
                runTask.paused = false;
                this.sharedState.runTask = null;
                this.doUpdateFar(item);
            } else {
                // 非运行任务强行停止
                this.doUpdateFar(item);
            }
        },
        doPauseFar(item) {
            let {runTask} = this.sharedState;
            if (runTask && !runTask.paused && runTask.id === item.id) {
                item.paused = true;
                runTask.paused = true;
            }
        },
        doResumeFar(item) {
            let {runTask} = this.sharedState;
            if (runTask && runTask.paused && runTask.id === item.id) {
                item.paused = false;
                runTask.paused = false;
            }
        },
        doTestFar(item) {
            this.editModel = deepClone(this.modelTest);
            this.isEdit = true;
            this.isTest = true;
        },
        doSubmitFarTest() {
            this.$api.taskFar.test(this.editModel).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '测试成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg + '\n' + res.data.data, duration: 10});
                    // this.isEdit = false;
                    // this.doGetAllFar()
                } else {
                    let msg = '测试失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '测试失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
    },
    watch: {
        list(newVal, oldVal) {
            console.log('list changed')
        },
        sharedState(newVal, oldVal) {
            console.log('sharedState changed')
        },
    },
};
var template = `
<div class="proxy">
    <h3>代理管理</h3>
    <button @click="doAdd" v-if="!isEdit">新增</button>
    <button @click="doCancel" v-if="isEdit">取消</button>
    <button @click="doRefresh">刷新</button>
    
    <Form :data="editModel" :showModel="formShowModel" :schema="schema" v-if="isEdit" @cancel="doCancel" @submit="doSubmit"></Form>
    <List :list="list" :showModel="listShowModel" :schema="schema">
        <template v-slot="{item}">
            <button @click="doEdit(item)">修改</button>
            <button @click="doDelete(item)">删除</button>
        </template>
    </List>
</div>
`;


let ProxyCom = {
    name: 'ProxyCom',
    data() {
        return {
            list: [],
            isEdit: false,
            schema: '代理配置',
            model: {
                proxyName: '代理-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                url: '',
                ip: '',
                port: 0,
                password: '',
                method: '',
                enabled: true,
            },
            listShowModel: {
                proxyName: '代理-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                url: {
                    type: 'text',
                    maxLength: 15,
                },
                ip: '',
                port: 0,
                password: '',
                method: '',
                enabled: {
                    success: true,
                },
                available: {
                    success: true,
                },
                updatedTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
            },
            formShowModel: {
                proxyName: '代理-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                url: '',
                ip: '',
                port: 0,
                password: '',
                method: '',
                enabled: {
                    type: 'boolean',
                },
            },
            editModel: {}
        }
    },
    components: {List, Form},
    template,
    mounted: () => {
        console.log('proxy mounted');
        const {proxy} = Vue.getCurrentInstance();
        proxy.doGetAll();
    },
    methods: {
        doRefresh() {
            this.$api.proxy.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                    let msg = '刷新成功';
                    this.$message({message: msg});
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doGetAll() {
            this.$api.proxy.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doAdd() {
            this.editModel = deepClone(this.model);
            this.isEdit = true;
        },
        doEdit(item) {
            this.editModel = deepClone(item);
            this.isEdit = true;
        },
        doDelete(item) {
            this.$api.proxy.delete(item).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '删除成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '删除失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '删除失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doSubmit() {
            this.$api.proxy.update(this.editModel).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '保存成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '保存失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '保存失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doCancel() {
            this.isEdit = false;
        },
        doStart() {
            alert('start')
        }
    }
};
var template = `
<div class="user">
    <h3>用户管理</h3>
    <button @click="doAdd" v-if="!isEdit">新增</button>
    <button @click="doCancel" v-if="isEdit">取消</button>
    <button @click="doRefresh">刷新</button>
    
    <Form :data="editModel" :showModel="formShowModel" :schema="schema" v-if="isEdit" @cancel="doCancel" @submit="doSubmit"></Form>
    <List :list="list" :showModel="listShowModel" :schema="schema">
        <template v-slot="{item}">
            <button @click="doEdit(item)">修改</button>
            <button @click="doDelete(item)">删除</button>
        </template>
    </List>
</div>
`;


let User = {
    name: 'User',
    data() {
        return {
            list: [],
            isEdit: false,
            schema: '用户信息',
            model: {
                username: '用户-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                password: '',
                cookies: '',
                enabled: true,
                baned: false,
            },
            listShowModel: {
                username: '用户-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                password: '',
                cookies: '',
                enabled: {
                    success: true,
                },
                baned: false,
                available: {
                    success: true,
                },
                updatedTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
            },
            formShowModel: {
                username: '用户-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                password: '',
                cookies: '',
                enabled: {
                    type: 'boolean',
                },
            },
            editModel: {}
        }
    },
    components: {List, Form},
    template,
    mounted: () => {
        console.log('user mounted');
        const {proxy} = Vue.getCurrentInstance();
        proxy.doGetAll();
    },
    methods: {
        doRefresh() {
            this.$api.user.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                    let msg = '刷新成功';
                    this.$message({message: msg});
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doGetAll() {
            this.$api.user.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doAdd() {
            this.editModel = deepClone(this.model);
            this.isEdit = true;
        },
        doEdit(item) {
            this.editModel = deepClone(item);
            this.isEdit = true;
        },
        doDelete(item) {
            this.$api.user.delete(item).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '删除成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '删除失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '删除失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doSubmit() {
            this.$api.user.update(this.editModel).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '保存成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '保存失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '保存失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doCancel() {
            this.isEdit = false;
        },
        doStart() {
            alert('start')
        }
    }
};
var template = `
<div class="notify-email">
    <h3>通知邮箱管理</h3>
    <button @click="doAdd" v-if="!isEdit">新增</button>
    <button @click="doCancel" v-if="isEdit">取消</button>
    <button @click="doRefresh">刷新</button>
    
    <Form :data="editModel" :showModel="formShowModel" :schema="schema" v-if="isEdit" @cancel="doCancel" @submit="doSubmit"></Form>
    <List :list="list" :showModel="listShowModel" :schema="schema">
        <template v-slot="{item}">
            <button @click="doEdit(item)">修改</button>
            <button @click="doDelete(item)">删除</button>
        </template>
    </List>
</div>
`;


let NotifyEmail = {
    name: 'NotifyEmail',
    data() {
        return {
            list: [],
            isEdit: false,
            schema: '通知邮箱信息',
            model: {
                email: 'mail@admin.com',
                enabled: true,
            },
            listShowModel: {
                email: 'mail@admin.com',
                enabled: {
                },
                updatedTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
            },
            formShowModel: {
                email: 'mail@admin.com',
                enabled: {
                    type: 'boolean',
                },
            },
            editModel: {}
        }
    },
    components: {List, Form},
    template,
    mounted: () => {
        console.log('user mounted');
        const {proxy} = Vue.getCurrentInstance();
        proxy.doGetAll();
    },
    methods: {
        doRefresh() {
            this.$api.notifyEmail.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                    let msg = '刷新成功';
                    this.$message({message: msg});
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doGetAll() {
            this.$api.notifyEmail.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doAdd() {
            this.editModel = deepClone(this.model);
            this.isEdit = true;
        },
        doEdit(item) {
            this.editModel = deepClone(item);
            this.isEdit = true;
        },
        doDelete(item) {
            this.$api.notifyEmail.delete(item).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '删除成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '删除失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '删除失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doSubmit() {
            this.$api.notifyEmail.update(this.editModel).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '保存成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '保存失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '保存失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doCancel() {
            this.isEdit = false;
        },
        doStart() {
            alert('start')
        }
    }
};
var template = `
<div class="config">
    <h3>设置管理</h3>
    <button @click="doAdd" v-if="!isEdit && list.length === 0">新增</button>
    <button @click="doCancel" v-if="isEdit">取消</button>
    <button @click="doRefresh">刷新</button>
    
    <Form :data="editModel" :showModel="formShowModel" :schema="schema" v-if="isEdit" @cancel="doCancel" @submit="doSubmit"></Form>
    <List :list="list" :showModel="listShowModel" :schema="schema">
        <template v-slot="{item}">
            <button @click="doEdit(item)">修改</button>
            <button @click="doDelete(item)">删除</button>
        </template>
    </List>
</div>
`;


let Config = {
    name: 'Config',
    data() {
        return {
            list: [],
            isEdit: false,
            schema: '系统配置',
            model: {
                threadSize: '',
                sleepTimeMs: '',
                sleepTaskMs: '',
                proxyCheckUrl: '',
                userCheckUrl: '',
                userCheck: false,
                defaultUserAgent: '',
                autoCreateTask: false,
                autoCreateTaskInterval: 0,
            },
            listShowModel: {
                threadSize: '',
                sleepTimeMs: '',
                sleepTaskMs: '',
                proxyCheckUrl: {
                    type: 'text',
                    maxLength: 15,
                },
                userCheckUrl: {
                    type: 'text',
                    maxLength: 15,
                },
                userCheck: false,
                defaultUserAgent: {
                    type: 'text',
                    maxLength: 15,
                },
                autoCreateTask: false,
                autoCreateTaskInterval: 0,
            },
            formShowModel: {
                threadSize: '',
                sleepTimeMs: '',
                sleepTaskMs: '',
                proxyCheckUrl: '',
                userCheckUrl: '',
                userCheck: {
                    type: 'boolean',
                },
                defaultUserAgent: '',
                autoCreateTask: {
                    type: 'boolean',
                },
                autoCreateTaskInterval: 0,
            },
            editModel: {}
        }
    },
    components: {List, Form},
    template,
    mounted: () => {
        console.log('config mounted');
        const {proxy} = Vue.getCurrentInstance();
        proxy.doGetAll();
    },
    methods: {
        doRefresh() {
            this.$api.config.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                    let msg = '刷新成功';
                    this.$message({message: msg});
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doGetAll() {
            this.$api.config.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doAdd() {
            this.editModel = deepClone(this.model);
            this.isEdit = true;
        },
        doEdit(item) {
            this.editModel = deepClone(item);
            this.isEdit = true;
        },
        doDelete(item) {
            this.$api.config.delete(item).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '删除成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '删除失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '删除失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doSubmit() {
            this.$api.config.update(this.editModel).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '保存成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '保存失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '保存失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doCancel() {
            this.isEdit = false;
        },
        doStart() {
            alert('start')
        }
    }
};
var template = `
<div v-is="'style'">
    {{cssData}}
</div>
`;


let Css = {
    name: 'Css',
    data() {
        return {
            cssData: `
        .task h2 {
            font-size: 14px;
            color: yellow;
        }
    `
        }
    },
    components: {},
    template,
    mounted: () => {
        const {proxy} = Vue.getCurrentInstance();
        console.log('%s mounted', proxy.$options.name);
        // proxy.doGetCss();
        proxy.cssData = `
#app-main .nav h1 {
    color: red;
}

#app-main .menu a {
    margin: 5px;
}

#app-main button {
    margin: 5px;
}

/*    message    */
#app-message {
    position: absolute;
    right: 5%;
    top: 5%;
    z-index: 99999;
}

#app-message .message .content {
    background: rgba(255, 255, 255, 0.7);
    border: solid 1px rgba(0, 0, 0, 0.7);
    border-radius: 10px;
    padding: 5px;
    margin: 5px;
    min-width: 120px;
    text-align: center;
    word-break: break-all;
    max-width: 200px;

}

#app-message .message .text {
    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15);
    margin-top: -2px;
}

#app-message .message button {
    border-radius: 10px;
    border: solid 1px black;
    height: 22px;
    width: 22px;
    position: relative;
    right: 0px;
    margin-top: -1px;
    float: right;
    margin-bottom: -2px;
}

#app-message .message .error {
    border-color: red;
    color: red;
}

#app-message .message .success {
    border-color: green;
    color: green;
}

/*config*/
#app-main .config h2 {
    font-size: 14px;
}

/*form*/
#app-main .form {
    margin-bottom: 5px;
    max-width: 400px;
    border: solid 1px;
}

#app-main .form .label {
    margin: 5px;
    float: left;
    max-width: 150px;
    min-width: 100px;
    padding: 1px;
}

#app-main .form .input {
    margin: 5px;
    float: right;
    width: 200px;
}

#app-main .form button {
    float: right;
}

#app-main .form .key {
    width: 64px;
}

#app-main .form .value {
    width: 84px;
}

#app-main .form .del {
    margin-left: -5px;
    margin-top: 2px;
    float: inherit;
    width: 22px;
}

#app-main .form .add {
    width: 22px;
    margin-right: 6px;
}

/*list*/
#app-main .list table {
    border-right: 1px solid;
    border-bottom: 1px solid;
}

#app-main .list table td, th {
    border-left: 1px solid;
    border-top: 1px solid;
    word-break: break-all;
}

#app-main .list table .green {
    background-color: green;
}

/*proxy*/
#app-main .proxy h2 {
    font-size: 14px;
}

/*task*/
#app-main .task h2 {
    font-size: 14px;
}

/*user*/
#app-main .user h2 {
    font-size: 14px;
}

/*user*/
#app-main .notify-email h2 {
    font-size: 14px;
}

/*runner*/
#app-main .runner hr {
    border: none;
}

/*task.health*/
#app-main .health .message {
    border: none;
}

#app-main .health .health-true {
    color: green;
}

#app-main .health .health-false {
    color: red;
}

#app-main .crack hr {
    border-style: solid;
    border-width: 1px 0px 0px 0px;
}

#app-main .crack .true {
    color: green;
}

#app-main .crack .crack-false {
    color: red;
}

`;
    },
    methods: {
        doGetCss() {
            this.$api.style.getApp().then(res => {
                if (res.status === 200 && res.data) {
                    this.cssData = res.data;
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        }
    }
};// <!-- 展示模板 -->
var template = `
<div>
    <div class="nav">
        <h1 class="title">{{appName}}!</h1>
        <p class="menu">
            <!--使用 router-link 组件进行导航 -->
            <!--通过传递 \`to\` 来指定链接 -->
            <!--\`<router-link>\` 将呈现一个带有正确 \`href\` 属性的 \`<a>\` 标签-->
            <router-link to="/">任务管理</router-link>
            <router-link to="/proxy">代理配置</router-link>
            <router-link to="/user">用户配置</router-link>
            <router-link to="/config">系统配置</router-link>
            <router-link to="/notify-email">通知邮箱</router-link>
        </p>
    </div>
    
    <!-- 路由出口 -->
    <!-- 路由匹配到的组件将渲染在这里 -->
    <router-view></router-view>
    <!-- <a href="https://www.jianshu.com/p/b0c16bab3388">vue3使用is和v-is</a> -->
    <div v-is="'style'" scoped>
    </div>
    <Css></Css>
<!--    <HeartBeat></HeartBeat>-->
</div>
`;
// <!-- Vue 代码 -->

let tbl = {
    // 下载的文件html
    page: 'far-page',
    // 待下载的文件html
    pageNot: 'far-page-not',
    // 所有的文件url
    url: 'far-url',
    // 商品的信息
    item: 'far-item',
    // 商品错误的信息
    itemErr: 'far-item-err',
    // 商品的原始信息
    itemOrigin: 'far-item-origin',
    // 商品的分类信息
    itemType: 'far-item-type',
    // 图片的文件
    img: 'far-img',
    // 图片错误的文件
    imgErr: 'far-img-err',
    // 图片不用下载的文件
    imgNot: 'far-img-not',
    // 下载错误的文件
    err: 'far-err',
};

// mongodb 数据状态
let tbs = {
    ok: 1,  // 正常
    fail: 2,  // 失败
    exclude: 3,  // 排除
    not: null,  // 未下载
}

// mongodb 查询指令
let tbc = {
    in: '$in',  // 数组
    notIn: '$nin',  // 不在
    equal: '$eq',  // 等于
    notEqual: '$ne', // 不等于
    greatThan: '$gt', // 大于
    greatThanEqual: '$gte', // >=
    lessThan: '$lt',  // 小于
    lessThanEqual: '$lte', // <=
    startsWith: '$regex', // 开头
    like: '$regex', // 模糊查询
    notLike: '$not',  // 不模糊查询
    all: '$all',  // 全部
    addList: '$addToSet',  // 增加列表
    size: '$size',  // 大小
    exists: '$exists',  // 存在
    notExists: '$not',  // 不存在
    type: '$type',  // 类型
    mod: '$mod',  // 模
    elemMatch: '$elemMatch',
    geoNear: '$geoNear',
    geoWithin: '$geoWithin',
    geoIntersects: '$geoIntersects',
    text: '$text',
    where: '$where',
    comment: '$comment',
    slice: '$slice',
    and: '$and',
    or: '$or',
    nor: '$nor',
    set: '$set',
    push: '$push',
    pull: '$pull',
    addToSet: '$addToSet',
    pop: '$pop',
    pullAll: '$pullAll',
    inc: '$inc',
    mul: '$mul',
    min: '$min',
    max: '$max',
    currentDate: '$currentDate',
    dateToString: '$dateToString',
    stringToDate: '$stringToDate',
    bit: '$bit',
    textSearch: '$textSearch',
    project: '$project',
    group: '$group',
    sort: '$sort',
    limit: '$limit',
    skip: '$skip',
    unwind: '$unwind',
    sample: '$sample',
    strLenBytes: '$strLenBytes',
    strLenCP: '$strLenCP',
    substr: '$substr',
    toLower: '$toLower',
    toUpper: '$toUpper',
    meta: '$meta',
    // 其他
    // 字符串
    str: '$str',
}

let App = {
    name: 'App',
    data: () => {
        return {
            sharedState: store.state,
            appName: 'Farfetch Crawler Admin',
            book: 'book1'
        }
    },
    components: {
        Css, HeartBeat
    },
    template: template,
    mounted: () => {
        const {proxy} = Vue.getCurrentInstance();
        console.log('%s mounted', proxy.$options.name);
        proxy.initApiDocs();
    },
    methods: {
        initApiDocs() {
            api.swagger.apiDocs().then(res => {
                if (res.status === 200 && res.data && res.data.components) {
                    this.sharedState.apiDocs = res.data;
                    console.log('load apiDocs', this.sharedState.apiDocs);
                }
            }).catch(e => {
                console.log('load apiDocs error', e)
            });
        }
    }
};
// 1. 定义路由组件.
// 也可以从其他文件导入
// const Task = { template: '<div>Task Page</div>' };
// const Proxy = { template: '<div>Proxy Page</div>' };

// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。
const routes = [
    {path: '/', component: Task},
    {path: '/proxy', component: ProxyCom},
    {path: '/config', component: Config},
    {path: '/user', component: User},
    {path: '/notify-email', component: NotifyEmail},
];

